2018-05-18 03:25:45 +00:00
/* globals jQuery, ActiveXObject */
2010-10-26 02:48:36 +00:00
/ * *
2010-11-05 17:00:32 +00:00
* Package : svgedit . utilities
2010-10-26 02:48:36 +00:00
*
2012-09-16 18:53:27 +00:00
* Licensed under the MIT License
2010-10-26 02:48:36 +00:00
*
* Copyright ( c ) 2010 Alexis Deveria
* Copyright ( c ) 2010 Jeff Schiller
* /
2018-05-18 03:25:45 +00:00
import './pathseg.js' ;
import RGBColor from './canvg/rgbcolor.js' ;
import { NS } from './svgedit.js' ;
import { getTransformList } from './svgtransformlist.js' ;
import { shortFloat , setUnitAttr , getTypeMap } from './units.js' ;
import {
hasMatrixTransform , transformListToTransform , transformBox
} from './math.js' ;
import {
isWebkit , supportsHVLineContainerBBox , supportsPathBBox , supportsXpath ,
supportsSelectors
} from './browser.js' ;
2010-10-26 02:48:36 +00:00
2010-10-29 05:09:39 +00:00
// Constants
2018-05-18 03:25:45 +00:00
const $ = jQuery ;
2010-10-29 05:09:39 +00:00
2010-10-26 02:48:36 +00:00
// String used to encode base64.
2018-05-18 03:25:45 +00:00
const KEYSTR = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=' ;
2010-10-26 02:48:36 +00:00
2010-11-05 15:29:30 +00:00
// Much faster than running getBBox() every time
2018-05-18 03:25:45 +00:00
const visElems = 'a,circle,ellipse,foreignObject,g,image,line,path,polygon,polyline,rect,svg,text,tspan,use' ;
const visElemsArr = visElems . split ( ',' ) ;
// const hidElems = 'clipPath,defs,desc,feGaussianBlur,filter,linearGradient,marker,mask,metadata,pattern,radialGradient,stop,switch,symbol,title,textPath';
2010-11-05 15:29:30 +00:00
2018-05-18 03:25:45 +00:00
let editorContext _ = null ;
let domdoc _ = null ;
let domcontainer _ = null ;
let svgroot _ = null ;
2010-12-02 17:14:24 +00:00
2018-05-18 03:25:45 +00:00
export const init = function ( editorContext ) {
2018-05-18 04:02:30 +00:00
editorContext _ = editorContext ;
domdoc _ = editorContext . getDOMDocument ( ) ;
domcontainer _ = editorContext . getDOMContainer ( ) ;
svgroot _ = editorContext . getSVGRoot ( ) ;
2010-12-02 17:14:24 +00:00
} ;
2010-10-26 02:48:36 +00:00
// Converts characters in a string to XML-friendly entities.
//
2013-02-15 17:02:24 +00:00
// Example: '&' becomes '&'
2010-10-26 02:48:36 +00:00
//
// Parameters:
// str - The string to be converted
//
// Returns:
// The converted string
2018-05-18 03:25:45 +00:00
export const toXml = function ( str ) {
2018-05-18 04:02:30 +00:00
// ' is ok in XML, but not HTML
// > does not normally need escaping, though it can if within a CDATA expression (and preceded by "]]")
return str . replace ( /&/g , '&' ) . replace ( /</g , '<' ) . replace ( />/g , '>' ) . replace ( /"/g , '"' ) . replace ( /'/ , ''' ) ;
2010-10-26 02:48:36 +00:00
} ;
2013-02-15 23:05:23 +00:00
// Converts XML entities in a string to single characters.
2013-02-15 17:02:24 +00:00
// Example: '&' becomes '&'
2010-10-26 02:48:36 +00:00
//
// Parameters:
// str - The string to be converted
//
2013-02-15 23:05:23 +00:00
// Returns:
2010-10-26 02:48:36 +00:00
// The converted string
2018-05-18 03:25:45 +00:00
export const fromXml = function ( str ) {
2018-05-18 04:02:30 +00:00
return $ ( '<p/>' ) . html ( str ) . text ( ) ;
2010-10-26 02:48:36 +00:00
} ;
// This code was written by Tyler Akins and has been placed in the
// public domain. It would be nice if you left this header intact.
// Base64 code from Tyler Akins -- http://rumkin.com
// schiller: Removed string concatenation in favour of Array.join() optimization,
2018-05-18 06:41:43 +00:00
// also precalculate the size of the array needed.
2010-10-26 02:48:36 +00:00
// Converts a string to base64
2018-05-18 03:25:45 +00:00
export const encode64 = function ( input ) {
2018-05-18 04:02:30 +00:00
// base64 strings are 4/3 larger than the original string
2018-05-18 03:25:45 +00:00
input = encodeUTF8 ( input ) ; // convert non-ASCII characters
// input = convertToXMLReferences(input);
2018-05-18 04:02:30 +00:00
if ( window . btoa ) {
return window . btoa ( input ) ; // Use native if available
}
2018-05-18 03:25:45 +00:00
const output = [ ] ;
2018-05-18 04:02:30 +00:00
output . length = Math . floor ( ( input . length + 2 ) / 3 ) * 4 ;
2018-05-18 03:25:45 +00:00
let i = 0 , p = 0 ;
2018-05-18 04:02:30 +00:00
do {
2018-05-18 03:25:45 +00:00
const chr1 = input . charCodeAt ( i ++ ) ;
const chr2 = input . charCodeAt ( i ++ ) ;
const chr3 = input . charCodeAt ( i ++ ) ;
const enc1 = chr1 >> 2 ;
const enc2 = ( ( chr1 & 3 ) << 4 ) | ( chr2 >> 4 ) ;
2018-05-18 04:02:30 +00:00
2018-05-18 03:25:45 +00:00
let enc3 = ( ( chr2 & 15 ) << 2 ) | ( chr3 >> 6 ) ;
let enc4 = chr3 & 63 ;
2018-05-18 04:02:30 +00:00
if ( isNaN ( chr2 ) ) {
enc3 = enc4 = 64 ;
} else if ( isNaN ( chr3 ) ) {
enc4 = 64 ;
}
output [ p ++ ] = KEYSTR . charAt ( enc1 ) ;
output [ p ++ ] = KEYSTR . charAt ( enc2 ) ;
output [ p ++ ] = KEYSTR . charAt ( enc3 ) ;
output [ p ++ ] = KEYSTR . charAt ( enc4 ) ;
} while ( i < input . length ) ;
return output . join ( '' ) ;
2010-10-26 02:48:36 +00:00
} ;
// Converts a string from base64
2018-05-18 03:25:45 +00:00
export const decode64 = function ( input ) {
2018-05-18 04:02:30 +00:00
if ( window . atob ) {
2018-05-18 03:25:45 +00:00
return decodeUTF8 ( window . atob ( input ) ) ;
2018-05-18 04:02:30 +00:00
}
// remove all characters that are not A-Z, a-z, 0-9, +, /, or =
input = input . replace ( /[^A-Za-z0-9+/=]/g , '' ) ;
2018-05-18 03:25:45 +00:00
let output = '' ;
let i = 0 ;
2018-05-18 04:02:30 +00:00
do {
2018-05-18 03:25:45 +00:00
const enc1 = KEYSTR . indexOf ( input . charAt ( i ++ ) ) ;
const enc2 = KEYSTR . indexOf ( input . charAt ( i ++ ) ) ;
const enc3 = KEYSTR . indexOf ( input . charAt ( i ++ ) ) ;
const enc4 = KEYSTR . indexOf ( input . charAt ( i ++ ) ) ;
2018-05-18 04:02:30 +00:00
2018-05-18 03:25:45 +00:00
const chr1 = ( enc1 << 2 ) | ( enc2 >> 4 ) ;
const chr2 = ( ( enc2 & 15 ) << 4 ) | ( enc3 >> 2 ) ;
const chr3 = ( ( enc3 & 3 ) << 6 ) | enc4 ;
2018-05-18 04:02:30 +00:00
2018-05-18 03:25:45 +00:00
output += String . fromCharCode ( chr1 ) ;
2018-05-18 04:02:30 +00:00
if ( enc3 !== 64 ) {
output = output + String . fromCharCode ( chr2 ) ;
}
if ( enc4 !== 64 ) {
output = output + String . fromCharCode ( chr3 ) ;
}
} while ( i < input . length ) ;
2018-05-18 03:25:45 +00:00
return decodeUTF8 ( output ) ;
2014-06-12 23:42:27 +00:00
} ;
2018-05-18 03:25:45 +00:00
export const decodeUTF8 = function ( argString ) {
2018-05-18 04:02:30 +00:00
return decodeURIComponent ( escape ( argString ) ) ;
2010-10-26 02:48:36 +00:00
} ;
2014-02-01 16:13:51 +00:00
// codedread:does not seem to work with webkit-based browsers on OSX // Brettz9: please test again as function upgraded
2018-05-18 03:25:45 +00:00
export const encodeUTF8 = function ( argString ) {
2018-05-18 04:02:30 +00:00
return unescape ( encodeURIComponent ( argString ) ) ;
2014-02-01 16:13:51 +00:00
} ;
2010-10-26 02:48:36 +00:00
2018-04-12 18:00:52 +00:00
/ * *
* convert dataURL to object URL
* @ param { string } dataurl
* @ return { string } object URL or empty string
* /
2018-05-18 03:25:45 +00:00
export const dataURLToObjectURL = function ( dataurl ) {
2018-05-18 04:02:30 +00:00
if ( typeof Uint8Array === 'undefined' || typeof Blob === 'undefined' || typeof URL === 'undefined' || ! URL . createObjectURL ) {
return '' ;
}
2018-05-18 03:25:45 +00:00
const arr = dataurl . split ( ',' ) , mime = arr [ 0 ] . match ( /:(.*?);/ ) [ 1 ] ,
bstr = atob ( arr [ 1 ] ) ;
let n = bstr . length ;
const u8arr = new Uint8Array ( n ) ;
2018-05-18 04:02:30 +00:00
while ( n -- ) {
u8arr [ n ] = bstr . charCodeAt ( n ) ;
}
2018-05-18 03:25:45 +00:00
const blob = new Blob ( [ u8arr ] , { type : mime } ) ;
2018-05-18 04:02:30 +00:00
return URL . createObjectURL ( blob ) ;
2018-04-12 18:00:52 +00:00
} ;
/ * *
* get object URL for a blob object
* @ param { Blob } blob A Blob object or File object
* @ return { string } object URL or empty string
* /
2018-05-18 03:25:45 +00:00
export const createObjectURL = function ( blob ) {
2018-05-18 04:02:30 +00:00
if ( ! blob || typeof URL === 'undefined' || ! URL . createObjectURL ) {
return '' ;
}
return URL . createObjectURL ( blob ) ;
2018-04-12 18:00:52 +00:00
} ;
/ * *
* @ property { string } blankPageObjectURL
* /
2018-05-18 03:25:45 +00:00
export const blankPageObjectURL = ( function ( ) {
2018-05-18 04:02:30 +00:00
if ( typeof Blob === 'undefined' ) {
return '' ;
}
2018-05-18 03:25:45 +00:00
const blob = new Blob ( [ '<html><head><title>SVG-edit</title></head><body> </body></html>' ] , { type : 'text/html' } ) ;
return createObjectURL ( blob ) ;
2018-05-16 00:53:27 +00:00
} ) ( ) ;
2018-04-12 18:00:52 +00:00
2010-10-26 02:48:36 +00:00
// Converts a string to use XML references
2018-05-18 03:25:45 +00:00
export const convertToXMLReferences = function ( input ) {
let n ,
2018-05-18 04:02:30 +00:00
output = '' ;
for ( n = 0 ; n < input . length ; n ++ ) {
2018-05-18 03:25:45 +00:00
const c = input . charCodeAt ( n ) ;
2018-05-18 04:02:30 +00:00
if ( c < 128 ) {
output += input [ n ] ;
} else if ( c > 127 ) {
output += ( '&#' + c + ';' ) ;
}
}
return output ;
2010-10-26 02:48:36 +00:00
} ;
// Cross-browser compatible method of converting a string to an XML tree
2018-05-18 03:25:45 +00:00
// found this function here: http://groups.google.com/group/jquery-dev/browse_thread/thread/c6d11387c580a77f
export const text2xml = function ( sXML ) {
if ( sXML . includes ( '<svg:svg' ) ) {
2018-05-18 04:02:30 +00:00
sXML = sXML . replace ( /<(\/?)svg:/g , '<$1' ) . replace ( 'xmlns:svg' , 'xmlns' ) ;
}
2018-05-18 03:25:45 +00:00
let out , dXML ;
2018-05-18 04:02:30 +00:00
try {
dXML = ( window . DOMParser ) ? new DOMParser ( ) : new ActiveXObject ( 'Microsoft.XMLDOM' ) ;
dXML . async = false ;
} catch ( e ) {
throw new Error ( 'XML Parser could not be instantiated' ) ;
}
try {
if ( dXML . loadXML ) {
out = ( dXML . loadXML ( sXML ) ) ? dXML : false ;
} else {
out = dXML . parseFromString ( sXML , 'text/xml' ) ;
}
} catch ( e2 ) { throw new Error ( 'Error parsing XML string' ) ; }
return out ;
2010-10-26 02:48:36 +00:00
} ;
// Converts a SVGRect into an object.
2018-05-16 00:53:27 +00:00
//
2010-10-26 02:48:36 +00:00
// Parameters:
// bbox - a SVGRect
2018-05-16 00:53:27 +00:00
//
2010-10-26 02:48:36 +00:00
// Returns:
// An object with properties names x, y, width, height.
2018-05-18 03:25:45 +00:00
export const bboxToObj = function ( bbox ) {
2018-05-18 04:02:30 +00:00
return {
x : bbox . x ,
y : bbox . y ,
width : bbox . width ,
height : bbox . height
} ;
2010-10-26 02:48:36 +00:00
} ;
2010-10-26 16:33:44 +00:00
2010-10-26 17:11:23 +00:00
// Walks the tree and executes the callback on each element in a top-down fashion
//
// Parameters:
// elem - DOM element to traverse
// cbFn - Callback function to run on each element
2018-05-18 03:25:45 +00:00
export const walkTree = function ( elem , cbFn ) {
2018-05-18 04:02:30 +00:00
if ( elem && elem . nodeType === 1 ) {
cbFn ( elem ) ;
2018-05-18 03:25:45 +00:00
let i = elem . childNodes . length ;
2018-05-18 04:02:30 +00:00
while ( i -- ) {
2018-05-18 03:25:45 +00:00
walkTree ( elem . childNodes . item ( i ) , cbFn ) ;
2018-05-18 04:02:30 +00:00
}
}
2010-10-26 17:11:23 +00:00
} ;
// Walks the tree and executes the callback on each element in a depth-first fashion
// TODO: FIXME: Shouldn't this be calling walkTreePost?
//
// Parameters:
// elem - DOM element to traverse
// cbFn - Callback function to run on each element
2018-05-18 03:25:45 +00:00
export const walkTreePost = function ( elem , cbFn ) {
2018-05-18 04:02:30 +00:00
if ( elem && elem . nodeType === 1 ) {
2018-05-18 03:25:45 +00:00
let i = elem . childNodes . length ;
2018-05-18 04:02:30 +00:00
while ( i -- ) {
2018-05-18 03:25:45 +00:00
walkTree ( elem . childNodes . item ( i ) , cbFn ) ;
2018-05-18 04:02:30 +00:00
}
cbFn ( elem ) ;
}
2010-10-26 17:11:23 +00:00
} ;
2013-02-15 23:05:23 +00:00
// Extracts the URL from the url(...) syntax of some attributes.
2010-10-26 17:11:23 +00:00
// Three variants:
2013-02-19 11:18:04 +00:00
// * <circle fill="url(someFile.svg#foo)" />
2010-10-26 17:11:23 +00:00
// * <circle fill="url('someFile.svg#foo')" />
// * <circle fill='url("someFile.svg#foo")' />
//
// Parameters:
// attrVal - The attribute value as a string
2013-02-15 23:05:23 +00:00
//
2010-10-26 17:11:23 +00:00
// Returns:
// String with just the URL, like someFile.svg#foo
2018-05-18 03:25:45 +00:00
export const getUrlFromAttr = function ( attrVal ) {
2018-05-18 04:02:30 +00:00
if ( attrVal ) {
// url("#somegrad")
2018-05-18 03:25:45 +00:00
if ( attrVal . startsWith ( 'url("' ) ) {
2018-05-18 04:02:30 +00:00
return attrVal . substring ( 5 , attrVal . indexOf ( '"' , 6 ) ) ;
}
// url('#somegrad')
2018-05-18 03:25:45 +00:00
if ( attrVal . startsWith ( "url('" ) ) {
2018-05-18 04:02:30 +00:00
return attrVal . substring ( 5 , attrVal . indexOf ( "'" , 6 ) ) ;
}
2018-05-18 03:25:45 +00:00
if ( attrVal . startsWith ( 'url(' ) ) {
2018-05-18 04:02:30 +00:00
return attrVal . substring ( 4 , attrVal . indexOf ( ')' ) ) ;
}
}
return null ;
2010-10-26 17:11:23 +00:00
} ;
2010-10-28 05:50:39 +00:00
// Returns the given element's xlink:href value
2018-05-18 03:25:45 +00:00
export const getHref = function ( elem ) {
2018-05-18 04:02:30 +00:00
return elem . getAttributeNS ( NS . XLINK , 'href' ) ;
2013-02-19 11:18:04 +00:00
} ;
2010-10-28 05:50:39 +00:00
// Sets the given element's xlink:href value
2018-05-18 03:25:45 +00:00
export const setHref = function ( elem , val ) {
2018-05-18 04:02:30 +00:00
elem . setAttributeNS ( NS . XLINK , 'xlink:href' , val ) ;
2013-02-19 11:18:04 +00:00
} ;
2010-10-28 05:50:39 +00:00
// Returns:
// The document's <defs> element, create it first if necessary
2018-05-18 03:25:45 +00:00
export const findDefs = function ( ) {
const svgElement = editorContext _ . getSVGContent ( ) ;
let defs = svgElement . getElementsByTagNameNS ( NS . SVG , 'defs' ) ;
2018-05-18 04:02:30 +00:00
if ( defs . length > 0 ) {
defs = defs [ 0 ] ;
} else {
defs = svgElement . ownerDocument . createElementNS ( NS . SVG , 'defs' ) ;
if ( svgElement . firstChild ) {
// first child is a comment, so call nextSibling
svgElement . insertBefore ( defs , svgElement . firstChild . nextSibling ) ;
} else {
svgElement . appendChild ( defs ) ;
}
}
return defs ;
2010-10-28 05:50:39 +00:00
} ;
2010-11-08 08:42:46 +00:00
// TODO(codedread): Consider moving the next to functions to bbox.js
2010-11-05 15:29:30 +00:00
// Get correct BBox for a path in Webkit
// Converted from code found here:
// http://blog.hackers-cafe.net/2009/06/how-to-calculate-bezier-curves-bounding.html
2013-02-15 23:05:23 +00:00
//
2010-11-05 15:29:30 +00:00
// Parameters:
// path - The path DOM element to get the BBox for
//
// Returns:
// A BBox-like object
2018-05-18 03:25:45 +00:00
export const getPathBBox = function ( path ) {
const seglist = path . pathSegList ;
const tot = seglist . numberOfItems ;
2018-05-18 04:02:30 +00:00
2018-05-18 03:25:45 +00:00
const bounds = [ [ ] , [ ] ] ;
const start = seglist . getItem ( 0 ) ;
let P0 = [ start . x , start . y ] ;
2018-05-18 04:02:30 +00:00
2018-05-18 03:25:45 +00:00
for ( let i = 0 ; i < tot ; i ++ ) {
const seg = seglist . getItem ( i ) ;
2018-05-18 04:02:30 +00:00
2018-05-18 03:25:45 +00:00
if ( seg . x === undefined ) { continue ; }
2018-05-18 04:02:30 +00:00
// Add actual points to limits
bounds [ 0 ] . push ( P0 [ 0 ] ) ;
bounds [ 1 ] . push ( P0 [ 1 ] ) ;
if ( seg . x1 ) {
2018-05-18 03:25:45 +00:00
const P1 = [ seg . x1 , seg . y1 ] ,
2018-05-18 04:02:30 +00:00
P2 = [ seg . x2 , seg . y2 ] ,
P3 = [ seg . x , seg . y ] ;
2018-05-18 03:25:45 +00:00
for ( let j = 0 ; j < 2 ; j ++ ) {
const calc = function ( t ) {
2018-05-18 04:02:30 +00:00
return Math . pow ( 1 - t , 3 ) * P0 [ j ] +
3 * Math . pow ( 1 - t , 2 ) * t * P1 [ j ] +
3 * ( 1 - t ) * Math . pow ( t , 2 ) * P2 [ j ] +
Math . pow ( t , 3 ) * P3 [ j ] ;
} ;
2018-05-18 03:25:45 +00:00
const b = 6 * P0 [ j ] - 12 * P1 [ j ] + 6 * P2 [ j ] ;
const a = - 3 * P0 [ j ] + 9 * P1 [ j ] - 9 * P2 [ j ] + 3 * P3 [ j ] ;
const c = 3 * P1 [ j ] - 3 * P0 [ j ] ;
2018-05-18 04:02:30 +00:00
if ( a === 0 ) {
if ( b === 0 ) {
continue ;
}
2018-05-18 03:25:45 +00:00
const t = - c / b ;
2018-05-18 04:02:30 +00:00
if ( t > 0 && t < 1 ) {
bounds [ j ] . push ( calc ( t ) ) ;
}
continue ;
}
2018-05-18 03:25:45 +00:00
const b2ac = Math . pow ( b , 2 ) - 4 * c * a ;
2018-05-18 04:02:30 +00:00
if ( b2ac < 0 ) { continue ; }
2018-05-18 03:25:45 +00:00
const t1 = ( - b + Math . sqrt ( b2ac ) ) / ( 2 * a ) ;
2018-05-18 04:02:30 +00:00
if ( t1 > 0 && t1 < 1 ) { bounds [ j ] . push ( calc ( t1 ) ) ; }
2018-05-18 03:25:45 +00:00
const t2 = ( - b - Math . sqrt ( b2ac ) ) / ( 2 * a ) ;
2018-05-18 04:02:30 +00:00
if ( t2 > 0 && t2 < 1 ) { bounds [ j ] . push ( calc ( t2 ) ) ; }
}
P0 = P3 ;
} else {
bounds [ 0 ] . push ( seg . x ) ;
bounds [ 1 ] . push ( seg . y ) ;
}
}
2018-05-18 03:25:45 +00:00
const x = Math . min . apply ( null , bounds [ 0 ] ) ;
const w = Math . max . apply ( null , bounds [ 0 ] ) - x ;
const y = Math . min . apply ( null , bounds [ 1 ] ) ;
const h = Math . max . apply ( null , bounds [ 1 ] ) - y ;
2018-05-18 04:02:30 +00:00
return {
2018-05-18 03:25:45 +00:00
x ,
y ,
width : w ,
height : h
2018-05-18 04:02:30 +00:00
} ;
2010-11-05 15:29:30 +00:00
} ;
2011-02-09 21:32:53 +00:00
// Get the given/selected element's bounding box object, checking for
// horizontal/vertical lines (see issue 717)
// Note that performance is currently terrible, so some way to improve would
// be great.
//
2013-02-15 23:05:23 +00:00
// Parameters:
2011-02-09 21:32:53 +00:00
// selected - Container or <use> DOM element
2018-05-16 00:53:27 +00:00
function groupBBFix ( selected ) {
2018-05-18 03:25:45 +00:00
if ( supportsHVLineContainerBBox ( ) ) {
2018-05-18 04:02:30 +00:00
try { return selected . getBBox ( ) ; } catch ( e ) { }
}
2018-05-18 03:25:45 +00:00
const ref = $ . data ( selected , 'ref' ) ;
let matched = null ;
let ret , copy ;
2018-05-18 04:02:30 +00:00
if ( ref ) {
copy = $ ( ref ) . children ( ) . clone ( ) . attr ( 'visibility' , 'hidden' ) ;
$ ( svgroot _ ) . append ( copy ) ;
matched = copy . filter ( 'line, path' ) ;
} else {
matched = $ ( selected ) . find ( 'line, path' ) ;
}
2018-05-18 03:25:45 +00:00
let issue = false ;
2018-05-18 04:02:30 +00:00
if ( matched . length ) {
matched . each ( function ( ) {
2018-05-18 03:25:45 +00:00
const bb = this . getBBox ( ) ;
2018-05-18 04:02:30 +00:00
if ( ! bb . width || ! bb . height ) {
issue = true ;
}
} ) ;
if ( issue ) {
2018-05-18 03:25:45 +00:00
const elems = ref ? copy : $ ( selected ) . children ( ) ;
2018-05-18 04:02:30 +00:00
ret = getStrokedBBox ( elems ) ; // getStrokedBBox defined in svgcanvas
} else {
ret = selected . getBBox ( ) ;
}
} else {
ret = selected . getBBox ( ) ;
}
if ( ref ) {
copy . remove ( ) ;
}
return ret ;
2011-02-09 21:32:53 +00:00
}
2010-11-05 15:29:30 +00:00
// Get the given/selected element's bounding box object, convert it to be more
// usable when necessary
//
// Parameters:
// elem - Optional DOM element to get the BBox for
2018-05-18 03:25:45 +00:00
export const getBBox = function ( elem ) {
const selected = elem || editorContext _ . geSelectedElements ( ) [ 0 ] ;
2018-05-18 04:02:30 +00:00
if ( elem . nodeType !== 1 ) { return null ; }
2018-05-18 03:25:45 +00:00
const elname = selected . nodeName ;
2018-05-18 04:02:30 +00:00
2018-05-18 03:25:45 +00:00
let ret = null ;
2018-05-18 04:02:30 +00:00
switch ( elname ) {
case 'text' :
if ( selected . textContent === '' ) {
selected . textContent = 'a' ; // Some character needed for the selector to use.
ret = selected . getBBox ( ) ;
selected . textContent = '' ;
} else {
if ( selected . getBBox ) { ret = selected . getBBox ( ) ; }
}
break ;
case 'path' :
2018-05-18 03:25:45 +00:00
if ( ! supportsPathBBox ( ) ) {
ret = getPathBBox ( selected ) ;
2018-05-18 04:02:30 +00:00
} else {
if ( selected . getBBox ) { ret = selected . getBBox ( ) ; }
}
break ;
case 'g' :
case 'a' :
ret = groupBBFix ( selected ) ;
break ;
default :
if ( elname === 'use' ) {
ret = groupBBFix ( selected , true ) ;
}
2018-05-18 03:25:45 +00:00
if ( elname === 'use' || ( elname === 'foreignObject' && isWebkit ( ) ) ) {
2018-05-18 04:02:30 +00:00
if ( ! ret ) { ret = selected . getBBox ( ) ; }
// This is resolved in later versions of webkit, perhaps we should
// have a featured detection for correct 'use' behavior?
// ——————————
2018-05-18 03:25:45 +00:00
if ( ! isWebkit ( ) ) {
const bb = { } ;
2018-05-18 04:02:30 +00:00
bb . width = ret . width ;
bb . height = ret . height ;
bb . x = ret . x + parseFloat ( selected . getAttribute ( 'x' ) || 0 ) ;
bb . y = ret . y + parseFloat ( selected . getAttribute ( 'y' ) || 0 ) ;
ret = bb ;
}
2018-05-18 03:25:45 +00:00
} else if ( visElemsArr . includes ( elname ) ) {
2018-05-18 04:02:30 +00:00
if ( selected ) {
try {
ret = selected . getBBox ( ) ;
} catch ( err ) {
// tspan (and textPath apparently) have no `getBBox` in Firefox: https://bugzilla.mozilla.org/show_bug.cgi?id=937268
// Re: Chrome returning bbox for containing text element, see: https://bugs.chromium.org/p/chromium/issues/detail?id=349835
2018-05-18 03:25:45 +00:00
const extent = selected . getExtentOfChar ( 0 ) ; // pos+dimensions of the first glyph
const width = selected . getComputedTextLength ( ) ; // width of the tspan
2018-05-18 04:02:30 +00:00
ret = {
x : extent . x ,
y : extent . y ,
2018-05-18 03:25:45 +00:00
width ,
2018-05-18 04:02:30 +00:00
height : extent . height
} ;
}
} else {
// Check if element is child of a foreignObject
2018-05-18 03:25:45 +00:00
const fo = $ ( selected ) . closest ( 'foreignObject' ) ;
2018-05-18 04:02:30 +00:00
if ( fo . length ) {
if ( fo [ 0 ] . getBBox ) {
ret = fo [ 0 ] . getBBox ( ) ;
}
}
}
}
}
if ( ret ) {
2018-05-18 03:25:45 +00:00
ret = bboxToObj ( ret ) ;
2018-05-18 04:02:30 +00:00
}
// get the bounding box from the DOM (which is in that element's coordinate system)
return ret ;
2010-11-05 15:29:30 +00:00
} ;
2016-04-26 20:01:39 +00:00
// Create a path 'd' attribute from path segments.
// Each segment is an array of the form: [singleChar, [x,y, x,y, ...]]
//
// Parameters:
// pathSegments - An array of path segments to be converted
//
// Returns:
// The converted path d attribute.
2018-05-18 03:25:45 +00:00
export const getPathDFromSegments = function ( pathSegments ) {
let d = '' ;
2018-05-18 04:02:30 +00:00
$ . each ( pathSegments , function ( j , seg ) {
2018-05-18 03:25:45 +00:00
const pts = seg [ 1 ] ;
2018-05-18 04:02:30 +00:00
d += seg [ 0 ] ;
2018-05-18 03:25:45 +00:00
for ( let i = 0 ; i < pts . length ; i += 2 ) {
2018-05-18 04:02:30 +00:00
d += ( pts [ i ] + ',' + pts [ i + 1 ] ) + ' ' ;
}
} ) ;
return d ;
2016-04-26 20:01:39 +00:00
} ;
2016-04-22 16:24:52 +00:00
2016-04-26 20:01:39 +00:00
// Make a path 'd' attribute from a simple SVG element shape.
//
// Parameters:
// elem - The element to be converted
//
// Returns:
// The path d attribute or undefined if the element type is unknown.
2018-05-18 03:25:45 +00:00
export const getPathDFromElement = function ( elem ) {
2018-05-18 04:02:30 +00:00
// Possibly the cubed root of 6, but 1.81 works best
2018-05-18 03:25:45 +00:00
let num = 1.81 ;
let d , a , rx , ry ;
2018-05-18 04:02:30 +00:00
switch ( elem . tagName ) {
case 'ellipse' :
case 'circle' :
a = $ ( elem ) . attr ( [ 'rx' , 'ry' , 'cx' , 'cy' ] ) ;
2018-05-18 03:25:45 +00:00
const { cx , cy } = a ;
( { rx , ry } = a ) ;
2018-05-18 04:02:30 +00:00
if ( elem . tagName === 'circle' ) {
rx = ry = $ ( elem ) . attr ( 'r' ) ;
}
2018-05-18 03:25:45 +00:00
d = getPathDFromSegments ( [
2018-05-18 04:02:30 +00:00
[ '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 = elem . getAttribute ( 'd' ) ;
break ;
case 'line' :
a = $ ( elem ) . attr ( [ 'x1' , 'y1' , 'x2' , 'y2' ] ) ;
d = 'M' + a . x1 + ',' + a . y1 + 'L' + a . x2 + ',' + a . y2 ;
break ;
case 'polyline' :
d = 'M' + elem . getAttribute ( 'points' ) ;
break ;
case 'polygon' :
d = 'M' + elem . getAttribute ( 'points' ) + ' Z' ;
break ;
case 'rect' :
2018-05-18 03:25:45 +00:00
const r = $ ( elem ) . attr ( [ 'rx' , 'ry' ] ) ;
( { rx , ry } = r ) ;
const b = elem . getBBox ( ) ;
const { x , y } = b , w = b . width , h = b . height ;
2018-05-18 04:02:30 +00:00
num = 4 - num ; // Why? Because!
if ( ! rx && ! ry ) {
// Regular rect
2018-05-18 03:25:45 +00:00
d = getPathDFromSegments ( [
2018-05-18 04:02:30 +00:00
[ 'M' , [ x , y ] ] ,
[ 'L' , [ x + w , y ] ] ,
[ 'L' , [ x + w , y + h ] ] ,
[ 'L' , [ x , y + h ] ] ,
[ 'L' , [ x , y ] ] ,
[ 'Z' , [ ] ]
] ) ;
} else {
2018-05-18 03:25:45 +00:00
d = getPathDFromSegments ( [
2018-05-18 04:02:30 +00:00
[ '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 ;
}
return d ;
2016-04-22 16:24:52 +00:00
} ;
2016-04-26 20:01:39 +00:00
// Get a set of attributes from an element that is useful for convertToPath.
//
// Parameters:
// elem - The element to be probed
//
// Returns:
// An object with attributes.
2018-05-18 03:25:45 +00:00
export const getExtraAttributesForConvertToPath = function ( elem ) {
const attrs = { } ;
2018-05-18 04:02:30 +00:00
// TODO: make this list global so that we can properly maintain it
// TODO: what about @transform, @clip-rule, @fill-rule, etc?
$ . each ( [ 'marker-start' , 'marker-end' , 'marker-mid' , 'filter' , 'clip-path' ] , function ( ) {
2018-05-18 03:25:45 +00:00
const a = elem . getAttribute ( this ) ;
2018-05-18 04:02:30 +00:00
if ( a ) {
attrs [ this ] = a ;
}
} ) ;
return attrs ;
2016-04-22 16:24:52 +00:00
} ;
// Get the BBox of an element-as-path
//
// Parameters:
// elem - The DOM element to be probed
2016-04-26 20:01:39 +00:00
// addSvgElementFromJson - Function to add the path element to the current layer. See canvas.addSvgElementFromJson
2016-04-22 16:24:52 +00:00
// pathActions - If a transform exists, pathActions.resetOrientation() is used. See: canvas.pathActions.
//
// Returns:
// The resulting path's bounding box object.
2018-05-18 03:25:45 +00:00
export const getBBoxOfElementAsPath = function ( elem , addSvgElementFromJson , pathActions ) {
const path = addSvgElementFromJson ( {
2018-05-18 04:02:30 +00:00
'element' : 'path' ,
2018-05-18 03:25:45 +00:00
'attr' : getExtraAttributesForConvertToPath ( elem )
2018-05-18 04:02:30 +00:00
} ) ;
2018-05-18 03:25:45 +00:00
const eltrans = elem . getAttribute ( 'transform' ) ;
2018-05-18 04:02:30 +00:00
if ( eltrans ) {
path . setAttribute ( 'transform' , eltrans ) ;
}
2018-05-18 03:25:45 +00:00
const parent = elem . parentNode ;
2018-05-18 04:02:30 +00:00
if ( elem . nextSibling ) {
parent . insertBefore ( path , elem ) ;
} else {
parent . appendChild ( path ) ;
}
2018-05-18 03:25:45 +00:00
const d = getPathDFromElement ( elem ) ;
2018-05-18 04:02:30 +00:00
if ( d ) path . setAttribute ( 'd' , d ) ;
else path . parentNode . removeChild ( path ) ;
// Get the correct BBox of the new path, then discard it
pathActions . resetOrientation ( path ) ;
2018-05-18 03:25:45 +00:00
let bb = false ;
2018-05-18 04:02:30 +00:00
try {
bb = path . getBBox ( ) ;
} catch ( e ) {
// Firefox fails
}
path . parentNode . removeChild ( path ) ;
return bb ;
2016-04-26 20:01:39 +00:00
} ;
2016-04-22 16:24:52 +00:00
// Convert selected element to a path.
//
// Parameters:
// elem - The DOM element to be converted
// attrs - Apply attributes to new path. see canvas.convertToPath
2016-04-26 20:01:39 +00:00
// addSvgElementFromJson - Function to add the path element to the current layer. See canvas.addSvgElementFromJson
2016-04-22 16:24:52 +00:00
// pathActions - If a transform exists, pathActions.resetOrientation() is used. See: canvas.pathActions.
// clearSelection - see canvas.clearSelection
// addToSelection - see canvas.addToSelection
// history - see svgedit.history
// addCommandToHistory - see canvas.addCommandToHistory
//
// Returns:
2016-04-22 17:39:20 +00:00
// The converted path element or null if the DOM element was not recognized.
2018-05-18 03:25:45 +00:00
export const convertToPath = function ( elem , attrs , addSvgElementFromJson , pathActions , clearSelection , addToSelection , history , addCommandToHistory ) {
const batchCmd = new history . BatchCommand ( 'Convert element to Path' ) ;
2018-05-18 04:02:30 +00:00
// Any attribute on the element not covered by the passed-in attributes
2018-05-18 03:25:45 +00:00
attrs = $ . extend ( { } , attrs , getExtraAttributesForConvertToPath ( elem ) ) ;
2018-05-18 04:02:30 +00:00
2018-05-18 03:25:45 +00:00
const path = addSvgElementFromJson ( {
element : 'path' ,
attr : attrs
2018-05-18 04:02:30 +00:00
} ) ;
2018-05-18 03:25:45 +00:00
const eltrans = elem . getAttribute ( 'transform' ) ;
2018-05-18 04:02:30 +00:00
if ( eltrans ) {
path . setAttribute ( 'transform' , eltrans ) ;
}
2018-05-18 03:25:45 +00:00
const { id } = elem ;
const parent = elem . parentNode ;
2018-05-18 04:02:30 +00:00
if ( elem . nextSibling ) {
parent . insertBefore ( path , elem ) ;
} else {
parent . appendChild ( path ) ;
}
2018-05-18 03:25:45 +00:00
const d = getPathDFromElement ( elem ) ;
2018-05-18 04:02:30 +00:00
if ( d ) {
path . setAttribute ( 'd' , d ) ;
// Replace the current element with the converted one
// Reorient if it has a matrix
if ( eltrans ) {
2018-05-18 03:25:45 +00:00
const tlist = getTransformList ( path ) ;
if ( hasMatrixTransform ( tlist ) ) {
2018-05-18 04:02:30 +00:00
pathActions . resetOrientation ( path ) ;
}
}
2018-05-18 03:25:45 +00:00
const { nextSibling } = elem ;
2018-05-18 04:02:30 +00:00
batchCmd . addSubCommand ( new history . RemoveElementCommand ( elem , nextSibling , parent ) ) ;
batchCmd . addSubCommand ( new history . InsertElementCommand ( path ) ) ;
clearSelection ( ) ;
elem . parentNode . removeChild ( elem ) ;
path . setAttribute ( 'id' , id ) ;
path . removeAttribute ( 'visibility' ) ;
addToSelection ( [ path ] , true ) ;
addCommandToHistory ( batchCmd ) ;
return path ;
} else {
// the elem.tagName was not recognized, so no "d" attribute. Remove it, so we've haven't changed anything.
path . parentNode . removeChild ( path ) ;
return null ;
}
2016-04-22 16:24:52 +00:00
} ;
2016-04-28 16:11:54 +00:00
// Can the bbox be optimized over the native getBBox? The optimized bbox is the same as the native getBBox when
// the rotation angle is a multiple of 90 degrees and there are no complex transforms.
// Getting an optimized bbox can be dramatically slower, so we want to make sure it's worth it.
//
// The best example for this is a circle rotate 45 degrees. The circle doesn't get wider or taller when rotated
// about it's center.
//
// The standard, unoptimized technique gets the native bbox of the circle, rotates the box 45 degrees, uses
// that width and height, and applies any transforms to get the final bbox. This means the calculated bbox
// is much wider than the original circle. If the angle had been 0, 90, 180, etc. both techniques render the
// same bbox.
//
// The optimization is not needed if the rotation is a multiple 90 degrees. The default technique is to call
// getBBox then apply the angle and any transforms.
//
// Parameters:
// angle - The rotation angle in degrees
// hasMatrixTransform - True if there is a matrix transform
//
// Returns:
// True if the bbox can be optimized.
2018-05-16 00:53:27 +00:00
function bBoxCanBeOptimizedOverNativeGetBBox ( angle , hasMatrixTransform ) {
2018-05-18 03:25:45 +00:00
const angleModulo90 = angle % 90 ;
const closeTo90 = angleModulo90 < - 89.99 || angleModulo90 > 89.99 ;
const closeTo0 = angleModulo90 > - 0.001 && angleModulo90 < 0.001 ;
2018-05-18 04:02:30 +00:00
return hasMatrixTransform || ! ( closeTo0 || closeTo90 ) ;
2016-04-28 16:11:54 +00:00
}
2016-04-24 20:43:20 +00:00
// Get bounding box that includes any transforms.
//
// Parameters:
// elem - The DOM element to be converted
// addSvgElementFromJson - Function to add the path element to the current layer. See canvas.addSvgElementFromJson
// pathActions - If a transform exists, pathActions.resetOrientation() is used. See: canvas.pathActions.
//
// Returns:
// A single bounding box object
2018-05-18 03:25:45 +00:00
export const getBBoxWithTransform = function ( elem , addSvgElementFromJson , pathActions ) {
2018-05-18 04:02:30 +00:00
// TODO: Fix issue with rotated groups. Currently they work
// fine in FF, but not in other browsers (same problem mentioned
// in Issue 339 comment #2).
2018-05-18 03:25:45 +00:00
let bb = getBBox ( elem ) ;
2018-05-18 04:02:30 +00:00
if ( ! bb ) {
return null ;
}
2018-05-18 03:25:45 +00:00
const tlist = getTransformList ( elem ) ;
const angle = getRotationAngleFromTransformList ( tlist ) ;
const hasMatrixXForm = hasMatrixTransform ( tlist ) ;
2018-05-18 04:02:30 +00:00
2018-05-18 03:25:45 +00:00
if ( angle || hasMatrixXForm ) {
let goodBb = false ;
if ( bBoxCanBeOptimizedOverNativeGetBBox ( angle , hasMatrixXForm ) ) {
2018-05-18 04:02:30 +00:00
// Get the BBox from the raw path for these elements
// TODO: why ellipse and not circle
2018-05-18 03:25:45 +00:00
const elemNames = [ 'ellipse' , 'path' , 'line' , 'polyline' , 'polygon' ] ;
if ( elemNames . includes ( elem . tagName ) ) {
bb = goodBb = getBBoxOfElementAsPath ( elem , addSvgElementFromJson , pathActions ) ;
2018-05-18 04:02:30 +00:00
} else if ( elem . tagName === 'rect' ) {
// Look for radius
2018-05-18 03:25:45 +00:00
const rx = elem . getAttribute ( 'rx' ) ;
const ry = elem . getAttribute ( 'ry' ) ;
2018-05-18 04:02:30 +00:00
if ( rx || ry ) {
2018-05-18 03:25:45 +00:00
bb = goodBb = getBBoxOfElementAsPath ( elem , addSvgElementFromJson , pathActions ) ;
2018-05-18 04:02:30 +00:00
}
}
}
if ( ! goodBb ) {
2018-05-18 03:25:45 +00:00
const { matrix } = transformListToTransform ( tlist ) ;
bb = transformBox ( bb . x , bb . y , bb . width , bb . height , matrix ) . aabox ;
2018-05-18 04:02:30 +00:00
// Old technique that was exceedingly slow with large documents.
//
// Accurate way to get BBox of rotated element in Firefox:
// Put element in group and get its BBox
//
// Must use clone else FF freaks out
2018-05-18 03:25:45 +00:00
// const clone = elem.cloneNode(true);
// const g = document.createElementNS(NS.SVG, 'g');
// const parent = elem.parentNode;
2018-05-18 04:02:30 +00:00
// parent.appendChild(g);
// g.appendChild(clone);
2018-05-18 03:25:45 +00:00
// const bb2 = bboxToObj(g.getBBox());
2018-05-18 04:02:30 +00:00
// parent.removeChild(g);
}
}
return bb ;
2016-04-24 20:43:20 +00:00
} ;
// TODO: This is problematic with large stroke-width and, for example, a single horizontal line. The calculated BBox extends way beyond left and right sides.
2018-05-16 00:53:27 +00:00
function getStrokeOffsetForBBox ( elem ) {
2018-05-18 03:25:45 +00:00
const sw = elem . getAttribute ( 'stroke-width' ) ;
2018-05-18 04:02:30 +00:00
return ( ! isNaN ( sw ) && elem . getAttribute ( 'stroke' ) !== 'none' ) ? sw / 2 : 0 ;
2018-05-17 18:57:28 +00:00
}
2016-04-24 20:43:20 +00:00
// Get the bounding box for one or more stroked and/or transformed elements
2010-11-05 15:59:30 +00:00
//
// Parameters:
2016-04-24 20:43:20 +00:00
// elems - Array with DOM elements to check
// addSvgElementFromJson - Function to add the path element to the current layer. See canvas.addSvgElementFromJson
// pathActions - If a transform exists, pathActions.resetOrientation() is used. See: canvas.pathActions.
//
// Returns:
// A single bounding box object
2018-05-18 03:25:45 +00:00
export const getStrokedBBox = function ( elems , addSvgElementFromJson , pathActions ) {
2018-05-18 04:02:30 +00:00
if ( ! elems || ! elems . length ) { return false ; }
2018-05-18 03:25:45 +00:00
let fullBb ;
2018-05-18 04:02:30 +00:00
$ . each ( elems , function ( ) {
if ( fullBb ) { return ; }
if ( ! this . parentNode ) { return ; }
2018-05-18 03:25:45 +00:00
fullBb = getBBoxWithTransform ( this , addSvgElementFromJson , pathActions ) ;
2018-05-18 04:02:30 +00:00
} ) ;
// This shouldn't ever happen...
if ( fullBb === undefined ) { return null ; }
// fullBb doesn't include the stoke, so this does no good!
// if (elems.length == 1) return fullBb;
2018-05-18 03:25:45 +00:00
let maxX = fullBb . x + fullBb . width ;
let maxY = fullBb . y + fullBb . height ;
let minX = fullBb . x ;
let minY = fullBb . y ;
2018-05-18 04:02:30 +00:00
// If only one elem, don't call the potentially slow getBBoxWithTransform method again.
if ( elems . length === 1 ) {
2018-05-18 03:25:45 +00:00
const offset = getStrokeOffsetForBBox ( elems [ 0 ] ) ;
2018-05-18 04:02:30 +00:00
minX -= offset ;
minY -= offset ;
maxX += offset ;
maxY += offset ;
} else {
$ . each ( elems , function ( i , elem ) {
2018-05-18 03:25:45 +00:00
const curBb = getBBoxWithTransform ( elem , addSvgElementFromJson , pathActions ) ;
2018-05-18 04:02:30 +00:00
if ( curBb ) {
2018-05-18 03:25:45 +00:00
const offset = getStrokeOffsetForBBox ( elem ) ;
2018-05-18 04:02:30 +00:00
minX = Math . min ( minX , curBb . x - offset ) ;
minY = Math . min ( minY , curBb . y - offset ) ;
// TODO: The old code had this test for max, but not min. I suspect this test should be for both min and max
if ( elem . nodeType === 1 ) {
maxX = Math . max ( maxX , curBb . x + curBb . width + offset ) ;
maxY = Math . max ( maxY , curBb . y + curBb . height + offset ) ;
}
}
} ) ;
}
fullBb . x = minX ;
fullBb . y = minY ;
fullBb . width = maxX - minX ;
fullBb . height = maxY - minY ;
return fullBb ;
2016-04-24 20:43:20 +00:00
} ;
// Get the rotation angle of the given transform list.
//
// Parameters:
// tlist - List of transforms
2018-05-16 00:53:27 +00:00
// toRad - Boolean that when true returns the value in radians rather than degrees
2010-11-05 15:59:30 +00:00
//
// Returns:
// Float with the angle in degrees or radians
2018-05-18 03:25:45 +00:00
export const getRotationAngleFromTransformList = function ( tlist , toRad ) {
2018-05-18 04:02:30 +00:00
if ( ! tlist ) { return 0 ; } // <svg> elements have no tlist
2018-05-18 03:25:45 +00:00
const N = tlist . numberOfItems ;
for ( let i = 0 ; i < N ; ++ i ) {
const xform = tlist . getItem ( i ) ;
2018-05-18 04:02:30 +00:00
if ( xform . type === 4 ) {
return toRad ? xform . angle * Math . PI / 180.0 : xform . angle ;
}
}
return 0.0 ;
2010-11-05 15:59:30 +00:00
} ;
2010-11-05 15:29:30 +00:00
2016-04-24 20:43:20 +00:00
// Get the rotation angle of the given/selected DOM element
//
// Parameters:
// elem - Optional DOM element to get the angle for
2018-05-16 00:53:27 +00:00
// toRad - Boolean that when true returns the value in radians rather than degrees
2016-04-24 20:43:20 +00:00
//
// Returns:
// Float with the angle in degrees or radians
2018-05-18 03:25:45 +00:00
export const getRotationAngle = function ( elem , toRad ) {
const selected = elem || editorContext _ . getSelectedElements ( ) [ 0 ] ;
2018-05-18 04:02:30 +00:00
// find the rotation transform (if any) and set it
2018-05-18 03:25:45 +00:00
const tlist = getTransformList ( selected ) ;
return getRotationAngleFromTransformList ( tlist , toRad ) ;
2016-04-24 20:43:20 +00:00
} ;
2013-02-14 15:19:46 +00:00
// Function getRefElem
// Get the reference element associated with the given attribute value
//
// Parameters:
// attrVal - The attribute value as a string
2018-05-18 03:25:45 +00:00
export const getRefElem = function ( attrVal ) {
return getElem ( getUrlFromAttr ( attrVal ) . substr ( 1 ) ) ;
2013-02-14 15:19:46 +00:00
} ;
2011-02-04 08:02:46 +00:00
// Get a DOM element by ID within the SVG root element.
//
// Parameters:
// id - String with the element's new ID
2018-05-18 03:25:45 +00:00
export const getElem = ( supportsSelectors ( ) )
? function ( id ) {
2018-05-18 04:02:30 +00:00
// querySelector lookup
return svgroot _ . querySelector ( '#' + id ) ;
2018-05-18 03:25:45 +00:00
} : supportsXpath ( )
? function ( id ) {
// xpath lookup
return domdoc _ . evaluate (
'svg:svg[@id="svgroot"]//svg:*[@id="' + id + '"]' ,
domcontainer _ ,
function ( ) { return NS . SVG ; } ,
9 ,
null ) . singleNodeValue ;
}
: function ( id ) {
// jQuery lookup: twice as slow as xpath in FF
return $ ( svgroot _ ) . find ( '[id=' + id + ']' ) [ 0 ] ;
} ;
2011-02-04 08:02:46 +00:00
// Assigns multiple attributes to an element.
//
2018-05-16 00:53:27 +00:00
// Parameters:
2011-02-04 08:02:46 +00:00
// node - DOM element to apply new attribute values to
// attrs - Object with attribute keys/values
// suspendLength - Optional integer of milliseconds to suspend redraw
// unitCheck - Boolean to indicate the need to use svgedit.units.setUnitAttr
2018-05-18 03:25:45 +00:00
export const assignAttributes = function ( node , attrs , suspendLength , unitCheck ) {
for ( const i in attrs ) {
const ns = ( i . substr ( 0 , 4 ) === 'xml:'
2018-05-18 04:02:30 +00:00
? NS . XML
: i . substr ( 0 , 6 ) === 'xlink:' ? NS . XLINK : null ) ;
if ( ns ) {
node . setAttributeNS ( ns , i , attrs [ i ] ) ;
} else if ( ! unitCheck ) {
node . setAttribute ( i , attrs [ i ] ) ;
} else {
2018-05-18 03:25:45 +00:00
setUnitAttr ( node , i , attrs [ i ] ) ;
2018-05-18 04:02:30 +00:00
}
}
2011-02-04 08:02:46 +00:00
} ;
2011-02-10 04:10:03 +00:00
// Remove unneeded (default) attributes, makes resulting SVG smaller
//
// Parameters:
// element - DOM element to clean up
2018-05-18 03:25:45 +00:00
export const cleanupElement = function ( element ) {
const defaults = {
2018-05-18 04:02:30 +00:00
'fill-opacity' : 1 ,
'stop-opacity' : 1 ,
'opacity' : 1 ,
'stroke' : 'none' ,
'stroke-dasharray' : 'none' ,
'stroke-linejoin' : 'miter' ,
'stroke-linecap' : 'butt' ,
'stroke-opacity' : 1 ,
'stroke-width' : 1 ,
'rx' : 0 ,
'ry' : 0
} ;
if ( element . nodeName === 'ellipse' ) {
// Ellipse elements requires rx and ry attributes
delete defaults . rx ;
delete defaults . ry ;
}
2018-05-18 03:25:45 +00:00
for ( const attr in defaults ) {
const val = defaults [ attr ] ;
2018-05-18 04:02:30 +00:00
if ( element . getAttribute ( attr ) === String ( val ) ) {
element . removeAttribute ( attr ) ;
}
}
2011-02-10 04:10:03 +00:00
} ;
2013-02-17 04:58:04 +00:00
// round value to for snapping
// NOTE: This function did not move to svgutils.js since it depends on curConfig.
2018-05-18 03:25:45 +00:00
export const snapToGrid = function ( value ) {
const unit = editorContext _ . getBaseUnit ( ) ;
let stepSize = editorContext _ . getSnappingStep ( ) ;
2018-05-18 04:02:30 +00:00
if ( unit !== 'px' ) {
2018-05-18 03:25:45 +00:00
stepSize *= getTypeMap ( ) [ unit ] ;
2018-05-18 04:02:30 +00:00
}
value = Math . round ( value / stepSize ) * stepSize ;
return value ;
2013-02-17 04:58:04 +00:00
} ;
2018-05-18 03:25:45 +00:00
export const regexEscape = function ( str , delimiter ) {
2018-05-18 04:02:30 +00:00
// From: http://phpjs.org/functions
return String ( str ) . replace ( new RegExp ( '[.\\\\+*?\\[\\^\\]$(){}=!<>|:\\' + ( delimiter || '' ) + '-]' , 'g' ) , '\\$&' ) ;
2014-02-19 01:37:21 +00:00
} ;
2014-05-22 10:21:29 +00:00
/ * *
* @ param { string } globalCheck A global which can be used to determine if the script is already loaded
* @ param { array } scripts An array of scripts to preload ( in order )
* @ param { function } cb The callback to execute upon load .
* /
2018-05-18 03:25:45 +00:00
export const executeAfterLoads = function ( globalCheck , scripts , cb ) {
2018-05-18 04:02:30 +00:00
return function ( ) {
2018-05-18 03:25:45 +00:00
const args = arguments ;
2018-05-18 04:02:30 +00:00
function endCallback ( ) {
cb . apply ( null , args ) ;
}
if ( window [ globalCheck ] ) {
endCallback ( ) ;
} else {
scripts . reduceRight ( function ( oldFunc , script ) {
return function ( ) {
2018-05-18 03:25:45 +00:00
// Todo: insert script with `s.type = 'module';` once modules
// widely supported (or can hopefully refactor these function
// and/or use `import()`)
2018-05-18 04:02:30 +00:00
$ . getScript ( script , oldFunc ) ;
} ;
} , endCallback ) ( ) ;
}
} ;
2014-05-22 10:21:29 +00:00
} ;
2018-05-18 03:25:45 +00:00
export const buildCanvgCallback = function ( callCanvg ) {
return executeAfterLoads ( 'canvg' , [ 'canvg/rgbcolor.js' , 'canvg/canvg.js' ] , callCanvg ) ;
2014-05-22 10:21:29 +00:00
} ;
2018-05-18 03:25:45 +00:00
export const buildJSPDFCallback = function ( callJSPDF ) {
return executeAfterLoads ( 'RGBColor' , [ 'canvg/rgbcolor.js' ] , function ( ) {
const arr = [ ] ;
if ( ! RGBColor || RGBColor . ok === undefined ) { // It's not our RGBColor, so we'll need to load it
2018-05-18 04:02:30 +00:00
arr . push ( 'canvg/rgbcolor.js' ) ;
}
2018-05-18 03:25:45 +00:00
executeAfterLoads ( 'jsPDF' , arr . concat ( 'jspdf/underscore-min.js' , 'jspdf/jspdf.min.js' , 'jspdf/jspdf.plugin.svgToPdf.js' ) , callJSPDF ) ( ) ;
2018-05-18 04:02:30 +00:00
} ) ;
2014-05-22 10:21:29 +00:00
} ;
2016-05-04 13:38:29 +00:00
/ * *
* Prevents default browser click behaviour on the given element
* @ param img - The DOM element to prevent the click on
* /
2018-05-18 03:25:45 +00:00
export const preventClickDefault = function ( img ) {
2018-05-18 04:02:30 +00:00
$ ( img ) . click ( function ( e ) { e . preventDefault ( ) ; } ) ;
2016-05-04 13:38:29 +00:00
} ;
/ * *
* Create a clone of an element , updating its ID and its children ' s IDs when needed
* @ param { Element } el - DOM element to clone
* @ param { function ( ) } getNextId - function the get the next unique ID .
* @ returns { Element }
* /
2018-05-18 03:25:45 +00:00
export const copyElem = function ( el , getNextId ) {
2018-05-18 04:02:30 +00:00
// manually create a copy of the element
2018-05-18 03:25:45 +00:00
const newEl = document . createElementNS ( el . namespaceURI , el . nodeName ) ;
2018-05-18 04:02:30 +00:00
$ . each ( el . attributes , function ( i , attr ) {
if ( attr . localName !== '-moz-math-font-style' ) {
newEl . setAttributeNS ( attr . namespaceURI , attr . nodeName , attr . value ) ;
}
} ) ;
// set the copied element's new id
newEl . removeAttribute ( 'id' ) ;
newEl . id = getNextId ( ) ;
// Opera's "d" value needs to be reset for Opera/Win/non-EN
// Also needed for webkit (else does not keep curved segments on clone)
2018-05-18 03:25:45 +00:00
if ( isWebkit ( ) && el . nodeName === 'path' ) {
const fixedD = convertPath ( el ) ;
2018-05-18 04:02:30 +00:00
newEl . setAttribute ( 'd' , fixedD ) ;
}
// now create copies of all children
$ . each ( el . childNodes , function ( i , child ) {
switch ( child . nodeType ) {
case 1 : // element node
2018-05-18 03:25:45 +00:00
newEl . appendChild ( copyElem ( child , getNextId ) ) ;
2018-05-18 04:02:30 +00:00
break ;
case 3 : // text node
newEl . textContent = child . nodeValue ;
break ;
default :
break ;
}
} ) ;
if ( $ ( el ) . data ( 'gsvg' ) ) {
$ ( newEl ) . data ( 'gsvg' , newEl . firstChild ) ;
} else if ( $ ( el ) . data ( 'symbol' ) ) {
2018-05-18 03:25:45 +00:00
const ref = $ ( el ) . data ( 'symbol' ) ;
2018-05-18 04:02:30 +00:00
$ ( newEl ) . data ( 'ref' , ref ) . data ( 'symbol' , ref ) ;
} else if ( newEl . tagName === 'image' ) {
2018-05-18 03:25:45 +00:00
preventClickDefault ( newEl ) ;
2018-05-18 04:02:30 +00:00
}
return newEl ;
2016-05-04 13:38:29 +00:00
} ;
/ * *
* TODO : refactor callers in convertPath to use getPathDFromSegments instead of this function .
* Legacy code refactored from svgcanvas . pathActions . convertPath
* @ param letter - path segment command
* @ param { Array . < Array . < number >> } points - x , y points .
* @ param { Array . < Array . < number >>= } morePoints - x , y points
* @ param { Array . < number >= } lastPoint - x , y point
* @ returns { string }
* /
2018-05-16 00:53:27 +00:00
function pathDSegment ( letter , points , morePoints , lastPoint ) {
2018-05-18 04:02:30 +00:00
$ . each ( points , function ( i , pnt ) {
2018-05-18 03:25:45 +00:00
points [ i ] = shortFloat ( pnt ) ;
2018-05-18 04:02:30 +00:00
} ) ;
2018-05-18 03:25:45 +00:00
let segment = letter + points . join ( ' ' ) ;
2018-05-18 04:02:30 +00:00
if ( morePoints ) {
segment += ' ' + morePoints . join ( ' ' ) ;
}
if ( lastPoint ) {
2018-05-18 03:25:45 +00:00
segment += ' ' + shortFloat ( lastPoint ) ;
2018-05-18 04:02:30 +00:00
}
return segment ;
2016-05-04 13:38:29 +00:00
}
// this is how we map paths to our preferred relative segment types
2018-05-18 03:25:45 +00:00
const pathMap = [ 0 , 'z' , 'M' , 'm' , 'L' , 'l' , 'C' , 'c' , 'Q' , 'q' , 'A' , 'a' ,
2018-05-18 04:02:30 +00:00
'H' , 'h' , 'V' , 'v' , 'S' , 's' , 'T' , 't' ] ;
2016-05-04 13:38:29 +00:00
/ * *
* TODO : move to pathActions . js when migrating rest of pathActions out of svgcanvas . js
* Convert a path to one with only absolute or relative values
* @ param { Object } path - the path to convert
* @ param { boolean } toRel - true of convert to relative
* @ returns { string }
* /
2018-05-18 03:25:45 +00:00
export const convertPath = function ( path , toRel ) {
const segList = path . pathSegList ;
const len = segList . numberOfItems ;
let curx = 0 , cury = 0 ;
let d = '' ;
let lastM = null ;
for ( let i = 0 ; i < len ; ++ i ) {
const seg = segList . getItem ( i ) ;
2018-05-18 04:02:30 +00:00
// if these properties are not in the segment, set them to zero
2018-05-18 03:25:45 +00:00
let x = seg . x || 0 ,
2018-05-18 04:02:30 +00:00
y = seg . y || 0 ,
x1 = seg . x1 || 0 ,
y1 = seg . y1 || 0 ,
x2 = seg . x2 || 0 ,
y2 = seg . y2 || 0 ;
2018-05-18 03:25:45 +00:00
const type = seg . pathSegType ;
let letter = pathMap [ type ] [ 'to' + ( toRel ? 'Lower' : 'Upper' ) + 'Case' ] ( ) ;
2018-05-18 04:02:30 +00:00
switch ( type ) {
case 1 : // z,Z closepath (Z/z)
d += 'z' ;
if ( lastM && ! toRel ) {
curx = lastM [ 0 ] ;
cury = lastM [ 1 ] ;
}
break ;
case 12 : // absolute horizontal line (H)
x -= curx ;
// Fallthrough
case 13 : // relative horizontal line (h)
if ( toRel ) {
curx += x ;
letter = 'l' ;
} else {
x += curx ;
curx = x ;
letter = 'L' ;
}
// Convert to "line" for easier editing
d += pathDSegment ( letter , [ [ x , cury ] ] ) ;
break ;
case 14 : // absolute vertical line (V)
y -= cury ;
// Fallthrough
case 15 : // relative vertical line (v)
if ( toRel ) {
cury += y ;
letter = 'l' ;
} else {
y += cury ;
cury = y ;
letter = 'L' ;
}
// Convert to "line" for easier editing
d += pathDSegment ( letter , [ [ curx , y ] ] ) ;
break ;
case 2 : // absolute move (M)
case 4 : // absolute line (L)
case 18 : // absolute smooth quad (T)
x -= curx ;
y -= cury ;
// Fallthrough
case 5 : // relative line (l)
case 3 : // relative move (m)
case 19 : // relative smooth quad (t)
if ( toRel ) {
curx += x ;
cury += y ;
} else {
x += curx ;
y += cury ;
curx = x ;
cury = y ;
}
if ( type === 2 || type === 3 ) { lastM = [ curx , cury ] ; }
d += pathDSegment ( letter , [ [ x , y ] ] ) ;
break ;
case 6 : // absolute cubic (C)
x -= curx ; x1 -= curx ; x2 -= curx ;
y -= cury ; y1 -= cury ; y2 -= cury ;
// Fallthrough
case 7 : // relative cubic (c)
if ( toRel ) {
curx += x ;
cury += y ;
} else {
x += curx ; x1 += curx ; x2 += curx ;
y += cury ; y1 += cury ; y2 += cury ;
curx = x ;
cury = y ;
}
d += pathDSegment ( letter , [ [ x1 , y1 ] , [ x2 , y2 ] , [ x , y ] ] ) ;
break ;
case 8 : // absolute quad (Q)
x -= curx ; x1 -= curx ;
y -= cury ; y1 -= cury ;
// Fallthrough
case 9 : // relative quad (q)
if ( toRel ) {
curx += x ;
cury += y ;
} else {
x += curx ; x1 += curx ;
y += cury ; y1 += cury ;
curx = x ;
cury = y ;
}
d += pathDSegment ( letter , [ [ x1 , y1 ] , [ x , y ] ] ) ;
break ;
case 10 : // absolute elliptical arc (A)
x -= curx ;
y -= cury ;
// Fallthrough
case 11 : // relative elliptical arc (a)
if ( toRel ) {
curx += x ;
cury += y ;
} else {
x += curx ;
y += cury ;
curx = x ;
cury = y ;
}
d += pathDSegment ( letter , [ [ seg . r1 , seg . r2 ] ] , [
seg . angle ,
( seg . largeArcFlag ? 1 : 0 ) ,
( seg . sweepFlag ? 1 : 0 )
] , [ x , y ] ) ;
break ;
case 16 : // absolute smooth cubic (S)
x -= curx ; x2 -= curx ;
y -= cury ; y2 -= cury ;
// Fallthrough
case 17 : // relative smooth cubic (s)
if ( toRel ) {
curx += x ;
cury += y ;
} else {
x += curx ; x2 += curx ;
y += cury ; y2 += cury ;
curx = x ;
cury = y ;
}
d += pathDSegment ( letter , [ [ x2 , y2 ] , [ x , y ] ] ) ;
break ;
} // switch on path segment type
} // for each segment
return d ;
2016-05-04 13:38:29 +00:00
} ;