UNPKG

three-stdlib

Version:

stand-alone library of threejs examples

1 lines 113 kB
{"version":3,"file":"SVGLoader.cjs","sources":["../../src/loaders/SVGLoader.js"],"sourcesContent":["import {\n Box2,\n BufferGeometry,\n FileLoader,\n Float32BufferAttribute,\n Loader,\n Matrix3,\n Path,\n Shape,\n ShapePath,\n ShapeUtils,\n Vector2,\n Vector3,\n} from 'three'\n\nclass SVGLoader extends Loader {\n constructor(manager) {\n super(manager)\n\n // Default dots per inch\n this.defaultDPI = 90\n\n // Accepted units: 'mm', 'cm', 'in', 'pt', 'pc', 'px'\n this.defaultUnit = 'px'\n }\n\n load(url, onLoad, onProgress, onError) {\n const scope = this\n\n const loader = new FileLoader(scope.manager)\n loader.setPath(scope.path)\n loader.setRequestHeader(scope.requestHeader)\n loader.setWithCredentials(scope.withCredentials)\n loader.load(\n url,\n function (text) {\n try {\n onLoad(scope.parse(text))\n } catch (e) {\n if (onError) {\n onError(e)\n } else {\n console.error(e)\n }\n\n scope.manager.itemError(url)\n }\n },\n onProgress,\n onError,\n )\n }\n\n parse(text) {\n const scope = this\n\n function parseNode(node, style) {\n if (node.nodeType !== 1) return\n\n const transform = getNodeTransform(node)\n\n let traverseChildNodes = true\n\n let path = null\n\n switch (node.nodeName) {\n case 'svg':\n break\n\n case 'style':\n parseCSSStylesheet(node)\n break\n\n case 'g':\n style = parseStyle(node, style)\n break\n\n case 'path':\n style = parseStyle(node, style)\n if (node.hasAttribute('d')) path = parsePathNode(node)\n break\n\n case 'rect':\n style = parseStyle(node, style)\n path = parseRectNode(node)\n break\n\n case 'polygon':\n style = parseStyle(node, style)\n path = parsePolygonNode(node)\n break\n\n case 'polyline':\n style = parseStyle(node, style)\n path = parsePolylineNode(node)\n break\n\n case 'circle':\n style = parseStyle(node, style)\n path = parseCircleNode(node)\n break\n\n case 'ellipse':\n style = parseStyle(node, style)\n path = parseEllipseNode(node)\n break\n\n case 'line':\n style = parseStyle(node, style)\n path = parseLineNode(node)\n break\n\n case 'defs':\n traverseChildNodes = false\n break\n\n case 'mask':\n traverseChildNodes = false\n break\n\n case 'use':\n style = parseStyle(node, style)\n const usedNodeId = node.href.baseVal.substring(1)\n const usedNode = node.viewportElement.getElementById(usedNodeId)\n if (usedNode) {\n parseNode(usedNode, style)\n } else {\n console.warn(\"SVGLoader: 'use node' references non-existent node id: \" + usedNodeId)\n }\n\n break\n\n default:\n // console.log( node );\n }\n\n if (path) {\n if (style.fill !== undefined && style.fill !== 'none') {\n path.color.setStyle(style.fill)\n }\n\n transformPath(path, currentTransform)\n\n paths.push(path)\n\n path.userData = { node: node, style: style }\n }\n\n if (traverseChildNodes) {\n const nodes = node.childNodes\n\n for (let i = 0; i < nodes.length; i++) {\n parseNode(nodes[i], style)\n }\n }\n\n if (transform) {\n transformStack.pop()\n\n if (transformStack.length > 0) {\n currentTransform.copy(transformStack[transformStack.length - 1])\n } else {\n currentTransform.identity()\n }\n }\n }\n\n function parsePathNode(node) {\n const path = new ShapePath()\n\n const point = new Vector2()\n const control = new Vector2()\n\n const firstPoint = new Vector2()\n let isFirstPoint = true\n let doSetFirstPoint = false\n\n const d = node.getAttribute('d')\n\n // console.log( d );\n\n const commands = d.match(/[a-df-z][^a-df-z]*/gi)\n\n for (let i = 0, l = commands.length; i < l; i++) {\n const command = commands[i]\n\n const type = command.charAt(0)\n const data = command.substr(1).trim()\n\n if (isFirstPoint === true) {\n doSetFirstPoint = true\n isFirstPoint = false\n }\n\n let numbers\n\n switch (type) {\n case 'M':\n numbers = parseFloats(data)\n for (let j = 0, jl = numbers.length; j < jl; j += 2) {\n point.x = numbers[j + 0]\n point.y = numbers[j + 1]\n control.x = point.x\n control.y = point.y\n\n if (j === 0) {\n path.moveTo(point.x, point.y)\n } else {\n path.lineTo(point.x, point.y)\n }\n\n if (j === 0) firstPoint.copy(point)\n }\n\n break\n\n case 'H':\n numbers = parseFloats(data)\n\n for (let j = 0, jl = numbers.length; j < jl; j++) {\n point.x = numbers[j]\n control.x = point.x\n control.y = point.y\n path.lineTo(point.x, point.y)\n\n if (j === 0 && doSetFirstPoint === true) firstPoint.copy(point)\n }\n\n break\n\n case 'V':\n numbers = parseFloats(data)\n\n for (let j = 0, jl = numbers.length; j < jl; j++) {\n point.y = numbers[j]\n control.x = point.x\n control.y = point.y\n path.lineTo(point.x, point.y)\n\n if (j === 0 && doSetFirstPoint === true) firstPoint.copy(point)\n }\n\n break\n\n case 'L':\n numbers = parseFloats(data)\n\n for (let j = 0, jl = numbers.length; j < jl; j += 2) {\n point.x = numbers[j + 0]\n point.y = numbers[j + 1]\n control.x = point.x\n control.y = point.y\n path.lineTo(point.x, point.y)\n\n if (j === 0 && doSetFirstPoint === true) firstPoint.copy(point)\n }\n\n break\n\n case 'C':\n numbers = parseFloats(data)\n\n for (let j = 0, jl = numbers.length; j < jl; j += 6) {\n path.bezierCurveTo(\n numbers[j + 0],\n numbers[j + 1],\n numbers[j + 2],\n numbers[j + 3],\n numbers[j + 4],\n numbers[j + 5],\n )\n control.x = numbers[j + 2]\n control.y = numbers[j + 3]\n point.x = numbers[j + 4]\n point.y = numbers[j + 5]\n\n if (j === 0 && doSetFirstPoint === true) firstPoint.copy(point)\n }\n\n break\n\n case 'S':\n numbers = parseFloats(data)\n\n for (let j = 0, jl = numbers.length; j < jl; j += 4) {\n path.bezierCurveTo(\n getReflection(point.x, control.x),\n getReflection(point.y, control.y),\n numbers[j + 0],\n numbers[j + 1],\n numbers[j + 2],\n numbers[j + 3],\n )\n control.x = numbers[j + 0]\n control.y = numbers[j + 1]\n point.x = numbers[j + 2]\n point.y = numbers[j + 3]\n\n if (j === 0 && doSetFirstPoint === true) firstPoint.copy(point)\n }\n\n break\n\n case 'Q':\n numbers = parseFloats(data)\n\n for (let j = 0, jl = numbers.length; j < jl; j += 4) {\n path.quadraticCurveTo(numbers[j + 0], numbers[j + 1], numbers[j + 2], numbers[j + 3])\n control.x = numbers[j + 0]\n control.y = numbers[j + 1]\n point.x = numbers[j + 2]\n point.y = numbers[j + 3]\n\n if (j === 0 && doSetFirstPoint === true) firstPoint.copy(point)\n }\n\n break\n\n case 'T':\n numbers = parseFloats(data)\n\n for (let j = 0, jl = numbers.length; j < jl; j += 2) {\n const rx = getReflection(point.x, control.x)\n const ry = getReflection(point.y, control.y)\n path.quadraticCurveTo(rx, ry, numbers[j + 0], numbers[j + 1])\n control.x = rx\n control.y = ry\n point.x = numbers[j + 0]\n point.y = numbers[j + 1]\n\n if (j === 0 && doSetFirstPoint === true) firstPoint.copy(point)\n }\n\n break\n\n case 'A':\n numbers = parseFloats(data, [3, 4], 7)\n\n for (let j = 0, jl = numbers.length; j < jl; j += 7) {\n // skip command if start point == end point\n if (numbers[j + 5] == point.x && numbers[j + 6] == point.y) continue\n\n const start = point.clone()\n point.x = numbers[j + 5]\n point.y = numbers[j + 6]\n control.x = point.x\n control.y = point.y\n parseArcCommand(\n path,\n numbers[j],\n numbers[j + 1],\n numbers[j + 2],\n numbers[j + 3],\n numbers[j + 4],\n start,\n point,\n )\n\n if (j === 0 && doSetFirstPoint === true) firstPoint.copy(point)\n }\n\n break\n\n case 'm':\n numbers = parseFloats(data)\n\n for (let j = 0, jl = numbers.length; j < jl; j += 2) {\n point.x += numbers[j + 0]\n point.y += numbers[j + 1]\n control.x = point.x\n control.y = point.y\n\n if (j === 0) {\n path.moveTo(point.x, point.y)\n } else {\n path.lineTo(point.x, point.y)\n }\n\n if (j === 0) firstPoint.copy(point)\n }\n\n break\n\n case 'h':\n numbers = parseFloats(data)\n\n for (let j = 0, jl = numbers.length; j < jl; j++) {\n point.x += numbers[j]\n control.x = point.x\n control.y = point.y\n path.lineTo(point.x, point.y)\n\n if (j === 0 && doSetFirstPoint === true) firstPoint.copy(point)\n }\n\n break\n\n case 'v':\n numbers = parseFloats(data)\n\n for (let j = 0, jl = numbers.length; j < jl; j++) {\n point.y += numbers[j]\n control.x = point.x\n control.y = point.y\n path.lineTo(point.x, point.y)\n\n if (j === 0 && doSetFirstPoint === true) firstPoint.copy(point)\n }\n\n break\n\n case 'l':\n numbers = parseFloats(data)\n\n for (let j = 0, jl = numbers.length; j < jl; j += 2) {\n point.x += numbers[j + 0]\n point.y += numbers[j + 1]\n control.x = point.x\n control.y = point.y\n path.lineTo(point.x, point.y)\n\n if (j === 0 && doSetFirstPoint === true) firstPoint.copy(point)\n }\n\n break\n\n case 'c':\n numbers = parseFloats(data)\n\n for (let j = 0, jl = numbers.length; j < jl; j += 6) {\n path.bezierCurveTo(\n point.x + numbers[j + 0],\n point.y + numbers[j + 1],\n point.x + numbers[j + 2],\n point.y + numbers[j + 3],\n point.x + numbers[j + 4],\n point.y + numbers[j + 5],\n )\n control.x = point.x + numbers[j + 2]\n control.y = point.y + numbers[j + 3]\n point.x += numbers[j + 4]\n point.y += numbers[j + 5]\n\n if (j === 0 && doSetFirstPoint === true) firstPoint.copy(point)\n }\n\n break\n\n case 's':\n numbers = parseFloats(data)\n\n for (let j = 0, jl = numbers.length; j < jl; j += 4) {\n path.bezierCurveTo(\n getReflection(point.x, control.x),\n getReflection(point.y, control.y),\n point.x + numbers[j + 0],\n point.y + numbers[j + 1],\n point.x + numbers[j + 2],\n point.y + numbers[j + 3],\n )\n control.x = point.x + numbers[j + 0]\n control.y = point.y + numbers[j + 1]\n point.x += numbers[j + 2]\n point.y += numbers[j + 3]\n\n if (j === 0 && doSetFirstPoint === true) firstPoint.copy(point)\n }\n\n break\n\n case 'q':\n numbers = parseFloats(data)\n\n for (let j = 0, jl = numbers.length; j < jl; j += 4) {\n path.quadraticCurveTo(\n point.x + numbers[j + 0],\n point.y + numbers[j + 1],\n point.x + numbers[j + 2],\n point.y + numbers[j + 3],\n )\n control.x = point.x + numbers[j + 0]\n control.y = point.y + numbers[j + 1]\n point.x += numbers[j + 2]\n point.y += numbers[j + 3]\n\n if (j === 0 && doSetFirstPoint === true) firstPoint.copy(point)\n }\n\n break\n\n case 't':\n numbers = parseFloats(data)\n\n for (let j = 0, jl = numbers.length; j < jl; j += 2) {\n const rx = getReflection(point.x, control.x)\n const ry = getReflection(point.y, control.y)\n path.quadraticCurveTo(rx, ry, point.x + numbers[j + 0], point.y + numbers[j + 1])\n control.x = rx\n control.y = ry\n point.x = point.x + numbers[j + 0]\n point.y = point.y + numbers[j + 1]\n\n if (j === 0 && doSetFirstPoint === true) firstPoint.copy(point)\n }\n\n break\n\n case 'a':\n numbers = parseFloats(data, [3, 4], 7)\n\n for (let j = 0, jl = numbers.length; j < jl; j += 7) {\n // skip command if no displacement\n if (numbers[j + 5] == 0 && numbers[j + 6] == 0) continue\n\n const start = point.clone()\n point.x += numbers[j + 5]\n point.y += numbers[j + 6]\n control.x = point.x\n control.y = point.y\n parseArcCommand(\n path,\n numbers[j],\n numbers[j + 1],\n numbers[j + 2],\n numbers[j + 3],\n numbers[j + 4],\n start,\n point,\n )\n\n if (j === 0 && doSetFirstPoint === true) firstPoint.copy(point)\n }\n\n break\n\n case 'Z':\n case 'z':\n path.currentPath.autoClose = true\n\n if (path.currentPath.curves.length > 0) {\n // Reset point to beginning of Path\n point.copy(firstPoint)\n path.currentPath.currentPoint.copy(point)\n isFirstPoint = true\n }\n\n break\n\n default:\n console.warn(command)\n }\n\n // console.log( type, parseFloats( data ), parseFloats( data ).length )\n\n doSetFirstPoint = false\n }\n\n return path\n }\n\n function parseCSSStylesheet(node) {\n if (!node.sheet || !node.sheet.cssRules || !node.sheet.cssRules.length) return\n\n for (let i = 0; i < node.sheet.cssRules.length; i++) {\n const stylesheet = node.sheet.cssRules[i]\n\n if (stylesheet.type !== 1) continue\n\n const selectorList = stylesheet.selectorText\n .split(/,/gm)\n .filter(Boolean)\n .map((i) => i.trim())\n\n for (let j = 0; j < selectorList.length; j++) {\n stylesheets[selectorList[j]] = Object.assign(stylesheets[selectorList[j]] || {}, stylesheet.style)\n }\n }\n }\n\n /**\n * https://www.w3.org/TR/SVG/implnote.html#ArcImplementationNotes\n * https://mortoray.com/2017/02/16/rendering-an-svg-elliptical-arc-as-bezier-curves/ Appendix: Endpoint to center arc conversion\n * From\n * rx ry x-axis-rotation large-arc-flag sweep-flag x y\n * To\n * aX, aY, xRadius, yRadius, aStartAngle, aEndAngle, aClockwise, aRotation\n */\n\n function parseArcCommand(path, rx, ry, x_axis_rotation, large_arc_flag, sweep_flag, start, end) {\n if (rx == 0 || ry == 0) {\n // draw a line if either of the radii == 0\n path.lineTo(end.x, end.y)\n return\n }\n\n x_axis_rotation = (x_axis_rotation * Math.PI) / 180\n\n // Ensure radii are positive\n rx = Math.abs(rx)\n ry = Math.abs(ry)\n\n // Compute (x1', y1')\n const dx2 = (start.x - end.x) / 2.0\n const dy2 = (start.y - end.y) / 2.0\n const x1p = Math.cos(x_axis_rotation) * dx2 + Math.sin(x_axis_rotation) * dy2\n const y1p = -Math.sin(x_axis_rotation) * dx2 + Math.cos(x_axis_rotation) * dy2\n\n // Compute (cx', cy')\n let rxs = rx * rx\n let rys = ry * ry\n const x1ps = x1p * x1p\n const y1ps = y1p * y1p\n\n // Ensure radii are large enough\n const cr = x1ps / rxs + y1ps / rys\n\n if (cr > 1) {\n // scale up rx,ry equally so cr == 1\n const s = Math.sqrt(cr)\n rx = s * rx\n ry = s * ry\n rxs = rx * rx\n rys = ry * ry\n }\n\n const dq = rxs * y1ps + rys * x1ps\n const pq = (rxs * rys - dq) / dq\n let q = Math.sqrt(Math.max(0, pq))\n if (large_arc_flag === sweep_flag) q = -q\n const cxp = (q * rx * y1p) / ry\n const cyp = (-q * ry * x1p) / rx\n\n // Step 3: Compute (cx, cy) from (cx', cy')\n const cx = Math.cos(x_axis_rotation) * cxp - Math.sin(x_axis_rotation) * cyp + (start.x + end.x) / 2\n const cy = Math.sin(x_axis_rotation) * cxp + Math.cos(x_axis_rotation) * cyp + (start.y + end.y) / 2\n\n // Step 4: Compute θ1 and Δθ\n const theta = svgAngle(1, 0, (x1p - cxp) / rx, (y1p - cyp) / ry)\n const delta = svgAngle((x1p - cxp) / rx, (y1p - cyp) / ry, (-x1p - cxp) / rx, (-y1p - cyp) / ry) % (Math.PI * 2)\n\n path.currentPath.absellipse(cx, cy, rx, ry, theta, theta + delta, sweep_flag === 0, x_axis_rotation)\n }\n\n function svgAngle(ux, uy, vx, vy) {\n const dot = ux * vx + uy * vy\n const len = Math.sqrt(ux * ux + uy * uy) * Math.sqrt(vx * vx + vy * vy)\n let ang = Math.acos(Math.max(-1, Math.min(1, dot / len))) // floating point precision, slightly over values appear\n if (ux * vy - uy * vx < 0) ang = -ang\n return ang\n }\n\n /*\n * According to https://www.w3.org/TR/SVG/shapes.html#RectElementRXAttribute\n * rounded corner should be rendered to elliptical arc, but bezier curve does the job well enough\n */\n function parseRectNode(node) {\n const x = parseFloatWithUnits(node.getAttribute('x') || 0)\n const y = parseFloatWithUnits(node.getAttribute('y') || 0)\n const rx = parseFloatWithUnits(node.getAttribute('rx') || node.getAttribute('ry') || 0)\n const ry = parseFloatWithUnits(node.getAttribute('ry') || node.getAttribute('rx') || 0)\n const w = parseFloatWithUnits(node.getAttribute('width'))\n const h = parseFloatWithUnits(node.getAttribute('height'))\n\n // Ellipse arc to Bezier approximation Coefficient (Inversed). See:\n // https://spencermortensen.com/articles/bezier-circle/\n const bci = 1 - 0.551915024494\n\n const path = new ShapePath()\n\n // top left\n path.moveTo(x + rx, y)\n\n // top right\n path.lineTo(x + w - rx, y)\n if (rx !== 0 || ry !== 0) {\n path.bezierCurveTo(x + w - rx * bci, y, x + w, y + ry * bci, x + w, y + ry)\n }\n\n // bottom right\n path.lineTo(x + w, y + h - ry)\n if (rx !== 0 || ry !== 0) {\n path.bezierCurveTo(x + w, y + h - ry * bci, x + w - rx * bci, y + h, x + w - rx, y + h)\n }\n\n // bottom left\n path.lineTo(x + rx, y + h)\n if (rx !== 0 || ry !== 0) {\n path.bezierCurveTo(x + rx * bci, y + h, x, y + h - ry * bci, x, y + h - ry)\n }\n\n // back to top left\n path.lineTo(x, y + ry)\n if (rx !== 0 || ry !== 0) {\n path.bezierCurveTo(x, y + ry * bci, x + rx * bci, y, x + rx, y)\n }\n\n return path\n }\n\n function parsePolygonNode(node) {\n function iterator(match, a, b) {\n const x = parseFloatWithUnits(a)\n const y = parseFloatWithUnits(b)\n\n if (index === 0) {\n path.moveTo(x, y)\n } else {\n path.lineTo(x, y)\n }\n\n index++\n }\n\n const regex = /(-?[\\d\\.?]+)[,|\\s](-?[\\d\\.?]+)/g\n\n const path = new ShapePath()\n\n let index = 0\n\n node.getAttribute('points').replace(regex, iterator)\n\n path.currentPath.autoClose = true\n\n return path\n }\n\n function parsePolylineNode(node) {\n function iterator(match, a, b) {\n const x = parseFloatWithUnits(a)\n const y = parseFloatWithUnits(b)\n\n if (index === 0) {\n path.moveTo(x, y)\n } else {\n path.lineTo(x, y)\n }\n\n index++\n }\n\n const regex = /(-?[\\d\\.?]+)[,|\\s](-?[\\d\\.?]+)/g\n\n const path = new ShapePath()\n\n let index = 0\n\n node.getAttribute('points').replace(regex, iterator)\n\n path.currentPath.autoClose = false\n\n return path\n }\n\n function parseCircleNode(node) {\n const x = parseFloatWithUnits(node.getAttribute('cx') || 0)\n const y = parseFloatWithUnits(node.getAttribute('cy') || 0)\n const r = parseFloatWithUnits(node.getAttribute('r') || 0)\n\n const subpath = new Path()\n subpath.absarc(x, y, r, 0, Math.PI * 2)\n\n const path = new ShapePath()\n path.subPaths.push(subpath)\n\n return path\n }\n\n function parseEllipseNode(node) {\n const x = parseFloatWithUnits(node.getAttribute('cx') || 0)\n const y = parseFloatWithUnits(node.getAttribute('cy') || 0)\n const rx = parseFloatWithUnits(node.getAttribute('rx') || 0)\n const ry = parseFloatWithUnits(node.getAttribute('ry') || 0)\n\n const subpath = new Path()\n subpath.absellipse(x, y, rx, ry, 0, Math.PI * 2)\n\n const path = new ShapePath()\n path.subPaths.push(subpath)\n\n return path\n }\n\n function parseLineNode(node) {\n const x1 = parseFloatWithUnits(node.getAttribute('x1') || 0)\n const y1 = parseFloatWithUnits(node.getAttribute('y1') || 0)\n const x2 = parseFloatWithUnits(node.getAttribute('x2') || 0)\n const y2 = parseFloatWithUnits(node.getAttribute('y2') || 0)\n\n const path = new ShapePath()\n path.moveTo(x1, y1)\n path.lineTo(x2, y2)\n path.currentPath.autoClose = false\n\n return path\n }\n\n //\n\n function parseStyle(node, style) {\n style = Object.assign({}, style) // clone style\n\n let stylesheetStyles = {}\n\n if (node.hasAttribute('class')) {\n const classSelectors = node\n .getAttribute('class')\n .split(/\\s/)\n .filter(Boolean)\n .map((i) => i.trim())\n\n for (let i = 0; i < classSelectors.length; i++) {\n stylesheetStyles = Object.assign(stylesheetStyles, stylesheets['.' + classSelectors[i]])\n }\n }\n\n if (node.hasAttribute('id')) {\n stylesheetStyles = Object.assign(stylesheetStyles, stylesheets['#' + node.getAttribute('id')])\n }\n\n function addStyle(svgName, jsName, adjustFunction) {\n if (adjustFunction === undefined) {\n adjustFunction = function copy(v) {\n if (v.startsWith('url')) console.warn('SVGLoader: url access in attributes is not implemented.')\n\n return v\n }\n }\n\n if (node.hasAttribute(svgName)) style[jsName] = adjustFunction(node.getAttribute(svgName))\n if (stylesheetStyles[svgName]) style[jsName] = adjustFunction(stylesheetStyles[svgName])\n if (node.style && node.style[svgName] !== '') style[jsName] = adjustFunction(node.style[svgName])\n }\n\n function clamp(v) {\n return Math.max(0, Math.min(1, parseFloatWithUnits(v)))\n }\n\n function positive(v) {\n return Math.max(0, parseFloatWithUnits(v))\n }\n\n addStyle('fill', 'fill')\n addStyle('fill-opacity', 'fillOpacity', clamp)\n addStyle('fill-rule', 'fillRule')\n addStyle('opacity', 'opacity', clamp)\n addStyle('stroke', 'stroke')\n addStyle('stroke-opacity', 'strokeOpacity', clamp)\n addStyle('stroke-width', 'strokeWidth', positive)\n addStyle('stroke-linejoin', 'strokeLineJoin')\n addStyle('stroke-linecap', 'strokeLineCap')\n addStyle('stroke-miterlimit', 'strokeMiterLimit', positive)\n addStyle('visibility', 'visibility')\n\n return style\n }\n\n // http://www.w3.org/TR/SVG11/implnote.html#PathElementImplementationNotes\n\n function getReflection(a, b) {\n return a - (b - a)\n }\n\n // from https://github.com/ppvg/svg-numbers (MIT License)\n\n function parseFloats(input, flags, stride) {\n if (typeof input !== 'string') {\n throw new TypeError('Invalid input: ' + typeof input)\n }\n\n // Character groups\n const RE = {\n SEPARATOR: /[ \\t\\r\\n\\,.\\-+]/,\n WHITESPACE: /[ \\t\\r\\n]/,\n DIGIT: /[\\d]/,\n SIGN: /[-+]/,\n POINT: /\\./,\n COMMA: /,/,\n EXP: /e/i,\n FLAGS: /[01]/,\n }\n\n // States\n const SEP = 0\n const INT = 1\n const FLOAT = 2\n const EXP = 3\n\n let state = SEP\n let seenComma = true\n let number = '',\n exponent = ''\n const result = []\n\n function throwSyntaxError(current, i, partial) {\n const error = new SyntaxError('Unexpected character \"' + current + '\" at index ' + i + '.')\n error.partial = partial\n throw error\n }\n\n function newNumber() {\n if (number !== '') {\n if (exponent === '') result.push(Number(number))\n else result.push(Number(number) * Math.pow(10, Number(exponent)))\n }\n\n number = ''\n exponent = ''\n }\n\n let current\n const length = input.length\n\n for (let i = 0; i < length; i++) {\n current = input[i]\n\n // check for flags\n if (Array.isArray(flags) && flags.includes(result.length % stride) && RE.FLAGS.test(current)) {\n state = INT\n number = current\n newNumber()\n continue\n }\n\n // parse until next number\n if (state === SEP) {\n // eat whitespace\n if (RE.WHITESPACE.test(current)) {\n continue\n }\n\n // start new number\n if (RE.DIGIT.test(current) || RE.SIGN.test(current)) {\n state = INT\n number = current\n continue\n }\n\n if (RE.POINT.test(current)) {\n state = FLOAT\n number = current\n continue\n }\n\n // throw on double commas (e.g. \"1, , 2\")\n if (RE.COMMA.test(current)) {\n if (seenComma) {\n throwSyntaxError(current, i, result)\n }\n\n seenComma = true\n }\n }\n\n // parse integer part\n if (state === INT) {\n if (RE.DIGIT.test(current)) {\n number += current\n continue\n }\n\n if (RE.POINT.test(current)) {\n number += current\n state = FLOAT\n continue\n }\n\n if (RE.EXP.test(current)) {\n state = EXP\n continue\n }\n\n // throw on double signs (\"-+1\"), but not on sign as separator (\"-1-2\")\n if (RE.SIGN.test(current) && number.length === 1 && RE.SIGN.test(number[0])) {\n throwSyntaxError(current, i, result)\n }\n }\n\n // parse decimal part\n if (state === FLOAT) {\n if (RE.DIGIT.test(current)) {\n number += current\n continue\n }\n\n if (RE.EXP.test(current)) {\n state = EXP\n continue\n }\n\n // throw on double decimal points (e.g. \"1..2\")\n if (RE.POINT.test(current) && number[number.length - 1] === '.') {\n throwSyntaxError(current, i, result)\n }\n }\n\n // parse exponent part\n if (state === EXP) {\n if (RE.DIGIT.test(current)) {\n exponent += current\n continue\n }\n\n if (RE.SIGN.test(current)) {\n if (exponent === '') {\n exponent += current\n continue\n }\n\n if (exponent.length === 1 && RE.SIGN.test(exponent)) {\n throwSyntaxError(current, i, result)\n }\n }\n }\n\n // end of number\n if (RE.WHITESPACE.test(current)) {\n newNumber()\n state = SEP\n seenComma = false\n } else if (RE.COMMA.test(current)) {\n newNumber()\n state = SEP\n seenComma = true\n } else if (RE.SIGN.test(current)) {\n newNumber()\n state = INT\n number = current\n } else if (RE.POINT.test(current)) {\n newNumber()\n state = FLOAT\n number = current\n } else {\n throwSyntaxError(current, i, result)\n }\n }\n\n // add the last number found (if any)\n newNumber()\n\n return result\n }\n\n // Units\n\n const units = ['mm', 'cm', 'in', 'pt', 'pc', 'px']\n\n // Conversion: [ fromUnit ][ toUnit ] (-1 means dpi dependent)\n const unitConversion = {\n mm: {\n mm: 1,\n cm: 0.1,\n in: 1 / 25.4,\n pt: 72 / 25.4,\n pc: 6 / 25.4,\n px: -1,\n },\n cm: {\n mm: 10,\n cm: 1,\n in: 1 / 2.54,\n pt: 72 / 2.54,\n pc: 6 / 2.54,\n px: -1,\n },\n in: {\n mm: 25.4,\n cm: 2.54,\n in: 1,\n pt: 72,\n pc: 6,\n px: -1,\n },\n pt: {\n mm: 25.4 / 72,\n cm: 2.54 / 72,\n in: 1 / 72,\n pt: 1,\n pc: 6 / 72,\n px: -1,\n },\n pc: {\n mm: 25.4 / 6,\n cm: 2.54 / 6,\n in: 1 / 6,\n pt: 72 / 6,\n pc: 1,\n px: -1,\n },\n px: {\n px: 1,\n },\n }\n\n function parseFloatWithUnits(string) {\n let theUnit = 'px'\n\n if (typeof string === 'string' || string instanceof String) {\n for (let i = 0, n = units.length; i < n; i++) {\n const u = units[i]\n\n if (string.endsWith(u)) {\n theUnit = u\n string = string.substring(0, string.length - u.length)\n break\n }\n }\n }\n\n let scale = undefined\n\n if (theUnit === 'px' && scope.defaultUnit !== 'px') {\n // Conversion scale from pixels to inches, then to default units\n\n scale = unitConversion['in'][scope.defaultUnit] / scope.defaultDPI\n } else {\n scale = unitConversion[theUnit][scope.defaultUnit]\n\n if (scale < 0) {\n // Conversion scale to pixels\n\n scale = unitConversion[theUnit]['in'] * scope.defaultDPI\n }\n }\n\n return scale * parseFloat(string)\n }\n\n // Transforms\n\n function getNodeTransform(node) {\n if (\n !(\n node.hasAttribute('transform') ||\n (node.nodeName === 'use' && (node.hasAttribute('x') || node.hasAttribute('y')))\n )\n ) {\n return null\n }\n\n const transform = parseNodeTransform(node)\n\n if (transformStack.length > 0) {\n transform.premultiply(transformStack[transformStack.length - 1])\n }\n\n currentTransform.copy(transform)\n transformStack.push(transform)\n\n return transform\n }\n\n function parseNodeTransform(node) {\n const transform = new Matrix3()\n const currentTransform = tempTransform0\n\n if (node.nodeName === 'use' && (node.hasAttribute('x') || node.hasAttribute('y'))) {\n const tx = parseFloatWithUnits(node.getAttribute('x'))\n const ty = parseFloatWithUnits(node.getAttribute('y'))\n\n transform.translate(tx, ty)\n }\n\n if (node.hasAttribute('transform')) {\n const transformsTexts = node.getAttribute('transform').split(')')\n\n for (let tIndex = transformsTexts.length - 1; tIndex >= 0; tIndex--) {\n const transformText = transformsTexts[tIndex].trim()\n\n if (transformText === '') continue\n\n const openParPos = transformText.indexOf('(')\n const closeParPos = transformText.length\n\n if (openParPos > 0 && openParPos < closeParPos) {\n const transformType = transformText.substr(0, openParPos)\n\n const array = parseFloats(transformText.substr(openParPos + 1, closeParPos - openParPos - 1))\n\n currentTransform.identity()\n\n switch (transformType) {\n case 'translate':\n if (array.length >= 1) {\n const tx = array[0]\n let ty = tx\n\n if (array.length >= 2) {\n ty = array[1]\n }\n\n currentTransform.translate(tx, ty)\n }\n\n break\n\n case 'rotate':\n if (array.length >= 1) {\n let angle = 0\n let cx = 0\n let cy = 0\n\n // Angle\n angle = (-array[0] * Math.PI) / 180\n\n if (array.length >= 3) {\n // Center x, y\n cx = array[1]\n cy = array[2]\n }\n\n // Rotate around center (cx, cy)\n tempTransform1.identity().translate(-cx, -cy)\n tempTransform2.identity().rotate(angle)\n tempTransform3.multiplyMatrices(tempTransform2, tempTransform1)\n tempTransform1.identity().translate(cx, cy)\n currentTransform.multiplyMatrices(tempTransform1, tempTransform3)\n }\n\n break\n\n case 'scale':\n if (array.length >= 1) {\n const scaleX = array[0]\n let scaleY = scaleX\n\n if (array.length >= 2) {\n scaleY = array[1]\n }\n\n currentTransform.scale(scaleX, scaleY)\n }\n\n break\n\n case 'skewX':\n if (array.length === 1) {\n currentTransform.set(1, Math.tan((array[0] * Math.PI) / 180), 0, 0, 1, 0, 0, 0, 1)\n }\n\n break\n\n case 'skewY':\n if (array.length === 1) {\n currentTransform.set(1, 0, 0, Math.tan((array[0] * Math.PI) / 180), 1, 0, 0, 0, 1)\n }\n\n break\n\n case 'matrix':\n if (array.length === 6) {\n currentTransform.set(array[0], array[2], array[4], array[1], array[3], array[5], 0, 0, 1)\n }\n\n break\n }\n }\n\n transform.premultiply(currentTransform)\n }\n }\n\n return transform\n }\n\n function transformPath(path, m) {\n function transfVec2(v2) {\n tempV3.set(v2.x, v2.y, 1).applyMatrix3(m)\n\n v2.set(tempV3.x, tempV3.y)\n }\n\n const isRotated = isTransformRotated(m)\n\n const subPaths = path.subPaths\n\n for (let i = 0, n = subPaths.length; i < n; i++) {\n const subPath = subPaths[i]\n const curves = subPath.curves\n\n for (let j = 0; j < curves.length; j++) {\n const curve = curves[j]\n\n if (curve.isLineCurve) {\n transfVec2(curve.v1)\n transfVec2(curve.v2)\n } else if (curve.isCubicBezierCurve) {\n transfVec2(curve.v0)\n transfVec2(curve.v1)\n transfVec2(curve.v2)\n transfVec2(curve.v3)\n } else if (curve.isQuadraticBezierCurve) {\n transfVec2(curve.v0)\n transfVec2(curve.v1)\n transfVec2(curve.v2)\n } else if (curve.isEllipseCurve) {\n if (isRotated) {\n console.warn('SVGLoader: Elliptic arc or ellipse rotation or skewing is not implemented.')\n }\n\n tempV2.set(curve.aX, curve.aY)\n transfVec2(tempV2)\n curve.aX = tempV2.x\n curve.aY = tempV2.y\n\n curve.xRadius *= getTransformScaleX(m)\n curve.yRadius *= getTransformScaleY(m)\n }\n }\n }\n }\n\n function isTransformRotated(m) {\n return m.elements[1] !== 0 || m.elements[3] !== 0\n }\n\n function getTransformScaleX(m) {\n const te = m.elements\n return Math.sqrt(te[0] * te[0] + te[1] * te[1])\n }\n\n function getTransformScaleY(m) {\n const te = m.elements\n return Math.sqrt(te[3] * te[3] + te[4] * te[4])\n }\n\n //\n\n const paths = []\n const stylesheets = {}\n\n const transformStack = []\n\n const tempTransform0 = new Matrix3()\n const tempTransform1 = new Matrix3()\n const tempTransform2 = new Matrix3()\n const tempTransform3 = new Matrix3()\n const tempV2 = new Vector2()\n const tempV3 = new Vector3()\n\n const currentTransform = new Matrix3()\n\n const xml = new DOMParser().parseFromString(text, 'image/svg+xml') // application/xml\n\n parseNode(xml.documentElement, {\n fill: '#000',\n fillOpacity: 1,\n strokeOpacity: 1,\n strokeWidth: 1,\n strokeLineJoin: 'miter',\n strokeLineCap: 'butt',\n strokeMiterLimit: 4,\n })\n\n const data = { paths: paths, xml: xml.documentElement }\n\n // console.log( paths );\n return data\n }\n\n static createShapes(shapePath) {\n // Param shapePath: a shapepath as returned by the parse function of this class\n // Returns Shape object\n\n const BIGNUMBER = 999999999\n\n const IntersectionLocationType = {\n ORIGIN: 0,\n DESTINATION: 1,\n BETWEEN: 2,\n LEFT: 3,\n RIGHT: 4,\n BEHIND: 5,\n BEYOND: 6,\n }\n\n const classifyResult = {\n loc: IntersectionLocationType.ORIGIN,\n t: 0,\n }\n\n function findEdgeIntersection(a0, a1, b0, b1) {\n const x1 = a0.x\n const x2 = a1.x\n const x3 = b0.x\n const x4 = b1.x\n const y1 = a0.y\n const y2 = a1.y\n const y3 = b0.y\n const y4 = b1.y\n const nom1 = (x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3)\n const nom2 = (x2 - x1) * (y1 - y3) - (y2 - y1) * (x1 - x3)\n const denom = (y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1)\n const t1 = nom1 / denom\n const t2 = nom2 / denom\n\n if ((denom === 0 && nom1 !== 0) || t1 <= 0 || t1 >= 1 || t2 < 0 || t2 > 1) {\n //1. lines are parallel or edges don't intersect\n\n return null\n } else if (nom1 === 0 && denom === 0) {\n //2. lines are colinear\n\n //check if endpoints of edge2 (b0-b1) lies on edge1 (a0-a1)\n for (let i = 0; i < 2; i++) {\n classifyPoint(i === 0 ? b0 : b1, a0, a1)\n //find position of this endpoints relatively to edge1\n if (classifyResult.loc == IntersectionLocationType.ORIGIN) {\n const point = i === 0 ? b0 : b1\n return { x: point.x, y: point.y, t: classifyResult.t }\n } else if (classifyResult.loc == IntersectionLocationType.BETWEEN) {\n const x = +(x1 + classifyResult.t * (x2 - x1)).toPrecision(10)\n const y = +(y1 + classifyResult.t * (y2 - y1)).toPrecision(10)\n return { x: x, y: y, t: classifyResult.t }\n }\n }\n\n return null\n } else {\n //3. edges intersect\n\n for (let i = 0; i < 2; i++) {\n classifyPoint(i === 0 ? b0 : b1, a0, a1)\n\n if (classifyResult.loc == IntersectionLocationType.ORIGIN) {\n const point = i === 0 ? b0 : b1\n return { x: point.x, y: point.y, t: classifyResult.t }\n }\n }\n\n const x = +(x1 + t1 * (x2 - x1)).toPrecision(10)\n const y = +(y1 + t1 * (y2 - y1)).toPrecision(10)\n return { x: x, y: y, t: t1 }\n }\n }\n\n function classifyPoint(p, edgeStart, edgeEnd) {\n const ax = edgeEnd.x - edgeStart.x\n const ay = edgeEnd.y - edgeStart.y\n const bx = p.x - edgeStart.x\n const by = p.y - edgeStart.y\n const sa = ax * by - bx * ay\n\n if (p.x === edgeStart.x && p.y === edgeStart.y) {\n classifyResult.loc = IntersectionLocationType.ORIGIN\n classifyResult.t = 0\n return\n }\n\n if (p.x === edgeEnd.x && p.y === edgeEnd.y) {\n classifyResult.loc = IntersectionLocationType.DESTINATION\n classifyResult.t = 1\n return\n }\n\n if (sa < -Number.EPSILON) {\n classifyResult.loc = IntersectionLocationType.LEFT\n return\n }\n\n if (sa > Number.EPSILON) {\n classifyResult.loc = IntersectionLocationType.RIGHT\n return\n }\n\n if (ax * bx < 0 || ay * by < 0) {\n classifyResult.loc = IntersectionLocationType.BEHIND\n return\n }\n\n if (Math.sqrt(ax * ax + ay * ay) < Math.sqrt(bx * bx + by * by)) {\n classifyResult.loc = IntersectionLocationType.BEYOND\n return\n }\n\n let t\n\n if (ax !== 0) {\n t = bx / ax\n } else {\n t = by / ay\n }\n\n classifyResult.loc = IntersectionLocationType.BETWEEN\n classifyResult.t = t\n }\n\n function getIntersections(path1, path2) {\n const intersectionsRaw = []\n const intersections = []\n\n for (let index = 1; index < path1.length; index++) {\n const path1EdgeStart = path1[index - 1]\n const path1EdgeEnd = path1[index]\n\n for (let index2 = 1; index2 < path2.length; index2++) {\n const path2EdgeStart = path2[index2 - 1]\n const path2EdgeEnd = path2[index2]\n\n const intersection = findEdgeIntersection(path1EdgeStart, path1EdgeEnd, path2EdgeStart, path2EdgeEnd)\n\n if (\n intersection !== null &&\n intersectionsRaw.find(\n (i) => i.t <= intersection.t + Number.EPSILON && i.t >= intersection.t - Number.EPSILON,\n ) === undefined\n ) {\n intersectionsRaw.push(intersection)\n intersections.push(new Vector2(intersection.x, intersection.y))\n }\n }\n }\n\n return intersections\n }\n\n function getScanlineIntersections(scanline, boundingBox, paths) {\n const center = new Vector2()\n boundingBox.getCenter(center)\n\n const allIntersections = []\n\n paths.forEach((path) => {\n // check if the center of the bounding box is in the bounding box of the paths.\n // this is a pruning method to limit the search of intersections in paths that can't envelop of the current path.\n // if a path envelops another path. The center of that oter path, has to be inside the bounding box of the enveloping path.\n if (path.boundingBox.containsPoint(center)) {\n const intersections = getIntersections(scanline, path.points)\n\n intersections.forEach((p) => {\n allIntersections.push({ identifier: path.identifier, isCW: path.isCW, point: p })\n })\n }\n })\n\n allIntersections.sort((i1, i2) => {\n return i1.point.x - i2.point.x\n })\n\n return allIntersections\n }\n\n function isHoleTo(simplePath, allPaths, scanlineMinX, scanlineMaxX, _fillRule) {\n if (_fillRule === null || _fillRule === undefined || _fillRule === '') {\n _fillRule = 'nonzero'\n }\n\n const centerBoundingBox = new Vector2()\n simplePath.boundingBox.getCenter(centerBoundingBox)\n\n const scanline = [new Vector2(scanlineMinX, centerBoundingBox.y), new Vector2(scanlineMaxX, centerBoundingBox.y)]\n\n const scanlineIntersections = getScanlineIntersections(scanline, simplePath.boundingBox, allPaths)\n\n scanlineIntersections.sort((i1, i2) => {\n return i1.point.x - i2.point.x\n })\n\n const baseIntersections = []\n const otherIntersections = []\n\n scanlineIntersections.forEach((i) => {\n if (i.identifier === simplePath.identifier) {\n baseIntersections.push(i)\n } else {\n otherIntersections.push(i)\n }\n })\n\n const firstXOfPath = baseIntersections[0].point.x\n\n // build up the path hierarchy\n const stack = []\n let i = 0\n\n while (i < otherIntersections.length && otherIntersections[i].point.x < firstXOfPath) {\n if (stack.length > 0 && stack[stack.length - 1] === otherIntersections[i].identifier) {\n stack.pop()\n } else {\n stack.push(otherIntersections[i].identifier)\n }\n\n i++\n }\n\n stack.push(simplePath.identifier)\n\n if (_fillRule === 'evenodd') {\n const isHole = stack.length % 2 === 0 ? true : false\n const isHoleFor = stack[stack.length - 2]\n\n return { identifier: simplePath.identifier, isHole: isHole, for: isHoleFor }\n } else if (_fillRule === 'nonzero') {\n // check if path is a hole by counting the amount of paths with alternating rotations it has to cross.\n let isHole = true\n let isHoleFor = null\n let lastCWValue = null\n\n for (let i = 0; i < stack.length; i++) {\n const identifier = stack[i]\n if (isHole) {\n lastCWValue = allPaths[identifier].isCW\n isHole = false\n isHoleFor = identifier\n } else if (lastCWValue !== allPaths[identifier].isCW) {\n lastCWValue = allPaths[identifier].isCW\n isHole = true\n }\n }\n\n return { identifier: simplePath.identifier, isHole: isHole, for: isHoleFor }\n } else {\n console.warn('fill-rule: \"' + _fillRule + '\" is currently not implemented.')\n }\n }\n\n // check for self intersecting paths\n // TODO\n\n // check intersecting paths\n // TODO\n\n // prepare paths for hole detection\n let identifier = 0\n\n let scanlineMinX = BIGNUMBER\n let scanlineMaxX = -BIGNUMBER\n\n let simplePaths = shapePath.subPaths.map((p) => {\n const points = p.getPoints()\n let maxY = -BIGNUMBER\n let minY = BIGNUMBER\n let maxX = -BIGNUMBER\n let minX = BIGNUMBER\n\n //points.forEach(p => p.y *= -1);\n\n for (let i = 0; i < points.length; i++) {\n const p = points[i]\n\n if (p.y > maxY) {\n maxY = p.y\n }\n\n if (p.y < minY) {\n minY = p.y\n }\n\n if (p.x > maxX) {\n maxX = p.x\n }\n\n if (p.x < minX) {\n minX = p.x\n }\n }\n\n //\n if (scanlineMaxX <= maxX) {\n scanlineMaxX = maxX + 1\n }\n\n if (scanlineMinX >= minX) {\n scanlineMinX = minX - 1\n }\n\n return {\n curves: p.curves,\n points: points,\n isCW: ShapeUtils.isClockWise(points),\n identifier: identifier++,\n boundingBox: new Box2(new Vector2(minX, minY), new Vector2(maxX, maxY)),\n }\n })\n\n simplePaths = simplePaths.filter((sp) => sp.points.length > 1)\n\n // check if path is solid or a hole\n const isAHole = simplePaths.map((p) =>\n isHoleTo(p, simplePaths, scanlineMinX, scanlineMaxX, shapePath.userData.style.fillRule),\n )\n\n const shapesToReturn = []\n simplePaths.forEach((p) => {\n const amIAHole = isAHole[p.identifier]\n\n if (!amIAHole.isHole) {\n const shape = new Shape()\n shape.curves = p.curves\n const holes = isAHole.filter((h) => h.isHole && h.for === p.identifier)\n holes.forEach((h) => {\n const hole = simplePaths[h.identifier]\n const path = new Path()\n path.curves = hole.curves\n shape.holes.push(path)\n })\n shapesToReturn.push(shape)\n }\n })\n\n return shapesToReturn\n }\n\n static getStrokeStyle(width, color, lineJoin, lineCap, miterLimit) {\n // Param width: Stroke width\n // Param color: As returned by THREE.Color.getStyle()\n // Param lineJoin: One of \"round\", \"bevel\", \"miter\" or \"miter-limit\"\n // Param lineCap: One of \"round\", \"square\" or \"butt\"\n // Param miterLimit: Maximum join length, in multiples of the \"width\" parameter (join is truncated if it exceeds that distance)\n // Returns style object\n\n width = width !== undefined ? width : 1\n color = color !== undefined ? color : '#000'\n lineJoin = lineJoin !== undefined ? lineJoin : 'miter'\n lineCap = lineCap !== und