UNPKG

kitchen-simulator

Version:

It is a kitchen simulator (self-contained micro-frontend).

1,326 lines (1,253 loc) 53.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.SVGLoader = void 0; var _three = require("three"); var SVGLoader = exports.SVGLoader = function SVGLoader(manager) { _three.Loader.call(this, manager); // Default dots per inch this.defaultDPI = 90; // Accepted units: 'mm', 'cm', 'in', 'pt', 'pc', 'px' this.defaultUnit = 'px'; }; SVGLoader.prototype = Object.assign(Object.create(_three.Loader.prototype), { constructor: SVGLoader, load: function load(url, onLoad, onProgress, onError) { var scope = this; var loader = new _three.FileLoader(scope.manager); loader.setPath(scope.path); loader.setRequestHeader(scope.requestHeader); loader.setWithCredentials(scope.withCredentials); loader.load(url, function (text) { try { onLoad(scope.parse(text)); } catch (e) { if (onError) { onError(e); } else { console.error(e); } scope.manager.itemError(url); } }, onProgress, onError); }, parse: function parse(text) { var scope = this; function parseNode(node, style) { if (node.nodeType !== 1) return; var transform = getNodeTransform(node); var traverseChildNodes = true; var path = null; switch (node.nodeName) { case 'svg': break; case 'style': parseCSSStylesheet(node); break; case 'g': style = parseStyle(node, style); break; case 'path': style = parseStyle(node, style); if (node.hasAttribute('d')) path = parsePathNode(node); break; case 'rect': style = parseStyle(node, style); path = parseRectNode(node); break; case 'polygon': style = parseStyle(node, style); path = parsePolygonNode(node); break; case 'polyline': style = parseStyle(node, style); path = parsePolylineNode(node); break; case 'circle': style = parseStyle(node, style); path = parseCircleNode(node); break; case 'ellipse': style = parseStyle(node, style); path = parseEllipseNode(node); break; case 'line': style = parseStyle(node, style); path = parseLineNode(node); break; case 'defs': traverseChildNodes = false; break; case 'use': style = parseStyle(node, style); var usedNodeId = node.href.baseVal.substring(1); var usedNode = node.viewportElement.getElementById(usedNodeId); if (usedNode) { parseNode(usedNode, style); } else { console.warn("SVGLoader: 'use node' references non-existent node id: " + usedNodeId); } break; default: // console.log( node ); } if (path) { if (style.fill !== undefined && style.fill !== 'none') { path.color.setStyle(style.fill); } transformPath(path, currentTransform); paths.push(path); path.userData = { node: node, style: style }; } if (traverseChildNodes) { var nodes = node.childNodes; for (var i = 0; i < nodes.length; i++) { parseNode(nodes[i], style); } } if (transform) { transformStack.pop(); if (transformStack.length > 0) { currentTransform.copy(transformStack[transformStack.length - 1]); } else { currentTransform.identity(); } } } function parsePathNode(node) { var path = new _three.ShapePath(); var point = new _three.Vector2(); var control = new _three.Vector2(); var firstPoint = new _three.Vector2(); var isFirstPoint = true; var doSetFirstPoint = false; var d = node.getAttribute('d'); // console.log( d ); var commands = d.match(/[a-df-z][^a-df-z]*/gi); for (var i = 0, l = commands.length; i < l; i++) { var command = commands[i]; var type = command.charAt(0); var data = command.substr(1).trim(); if (isFirstPoint === true) { doSetFirstPoint = true; isFirstPoint = false; } switch (type) { case 'M': var numbers = parseFloats(data); for (var j = 0, jl = numbers.length; j < jl; j += 2) { point.x = numbers[j + 0]; point.y = numbers[j + 1]; control.x = point.x; control.y = point.y; if (j === 0) { path.moveTo(point.x, point.y); } else { path.lineTo(point.x, point.y); } if (j === 0 && doSetFirstPoint === true) firstPoint.copy(point); } break; case 'H': var numbers = parseFloats(data); for (var j = 0, jl = numbers.length; j < jl; j++) { point.x = numbers[j]; control.x = point.x; control.y = point.y; path.lineTo(point.x, point.y); if (j === 0 && doSetFirstPoint === true) firstPoint.copy(point); } break; case 'V': var numbers = parseFloats(data); for (var j = 0, jl = numbers.length; j < jl; j++) { point.y = numbers[j]; control.x = point.x; control.y = point.y; path.lineTo(point.x, point.y); if (j === 0 && doSetFirstPoint === true) firstPoint.copy(point); } break; case 'L': var numbers = parseFloats(data); for (var j = 0, jl = numbers.length; j < jl; j += 2) { point.x = numbers[j + 0]; point.y = numbers[j + 1]; control.x = point.x; control.y = point.y; path.lineTo(point.x, point.y); if (j === 0 && doSetFirstPoint === true) firstPoint.copy(point); } break; case 'C': var numbers = parseFloats(data); for (var j = 0, jl = numbers.length; j < jl; j += 6) { path.bezierCurveTo(numbers[j + 0], numbers[j + 1], numbers[j + 2], numbers[j + 3], numbers[j + 4], numbers[j + 5]); control.x = numbers[j + 2]; control.y = numbers[j + 3]; point.x = numbers[j + 4]; point.y = numbers[j + 5]; if (j === 0 && doSetFirstPoint === true) firstPoint.copy(point); } break; case 'S': var numbers = parseFloats(data); for (var j = 0, jl = numbers.length; j < jl; j += 4) { path.bezierCurveTo(getReflection(point.x, control.x), getReflection(point.y, control.y), numbers[j + 0], numbers[j + 1], numbers[j + 2], numbers[j + 3]); control.x = numbers[j + 0]; control.y = numbers[j + 1]; point.x = numbers[j + 2]; point.y = numbers[j + 3]; if (j === 0 && doSetFirstPoint === true) firstPoint.copy(point); } break; case 'Q': var numbers = parseFloats(data); for (var j = 0, jl = numbers.length; j < jl; j += 4) { path.quadraticCurveTo(numbers[j + 0], numbers[j + 1], numbers[j + 2], numbers[j + 3]); control.x = numbers[j + 0]; control.y = numbers[j + 1]; point.x = numbers[j + 2]; point.y = numbers[j + 3]; if (j === 0 && doSetFirstPoint === true) firstPoint.copy(point); } break; case 'T': var numbers = parseFloats(data); for (var j = 0, jl = numbers.length; j < jl; j += 2) { var rx = getReflection(point.x, control.x); var ry = getReflection(point.y, control.y); path.quadraticCurveTo(rx, ry, numbers[j + 0], numbers[j + 1]); control.x = rx; control.y = ry; point.x = numbers[j + 0]; point.y = numbers[j + 1]; if (j === 0 && doSetFirstPoint === true) firstPoint.copy(point); } break; case 'A': var numbers = parseFloats(data); for (var j = 0, jl = numbers.length; j < jl; j += 7) { var start = point.clone(); point.x = numbers[j + 5]; point.y = numbers[j + 6]; control.x = point.x; control.y = point.y; parseArcCommand(path, numbers[j], numbers[j + 1], numbers[j + 2], numbers[j + 3], numbers[j + 4], start, point); if (j === 0 && doSetFirstPoint === true) firstPoint.copy(point); } break; case 'm': var numbers = parseFloats(data); for (var j = 0, jl = numbers.length; j < jl; j += 2) { point.x += numbers[j + 0]; point.y += numbers[j + 1]; control.x = point.x; control.y = point.y; if (j === 0) { path.moveTo(point.x, point.y); } else { path.lineTo(point.x, point.y); } if (j === 0 && doSetFirstPoint === true) firstPoint.copy(point); } break; case 'h': var numbers = parseFloats(data); for (var j = 0, jl = numbers.length; j < jl; j++) { point.x += numbers[j]; control.x = point.x; control.y = point.y; path.lineTo(point.x, point.y); if (j === 0 && doSetFirstPoint === true) firstPoint.copy(point); } break; case 'v': var numbers = parseFloats(data); for (var j = 0, jl = numbers.length; j < jl; j++) { point.y += numbers[j]; control.x = point.x; control.y = point.y; path.lineTo(point.x, point.y); if (j === 0 && doSetFirstPoint === true) firstPoint.copy(point); } break; case 'l': var numbers = parseFloats(data); for (var j = 0, jl = numbers.length; j < jl; j += 2) { point.x += numbers[j + 0]; point.y += numbers[j + 1]; control.x = point.x; control.y = point.y; path.lineTo(point.x, point.y); if (j === 0 && doSetFirstPoint === true) firstPoint.copy(point); } break; case 'c': var numbers = parseFloats(data); for (var j = 0, jl = numbers.length; j < jl; j += 6) { path.bezierCurveTo(point.x + numbers[j + 0], point.y + numbers[j + 1], point.x + numbers[j + 2], point.y + numbers[j + 3], point.x + numbers[j + 4], point.y + numbers[j + 5]); control.x = point.x + numbers[j + 2]; control.y = point.y + numbers[j + 3]; point.x += numbers[j + 4]; point.y += numbers[j + 5]; if (j === 0 && doSetFirstPoint === true) firstPoint.copy(point); } break; case 's': var numbers = parseFloats(data); for (var j = 0, jl = numbers.length; j < jl; j += 4) { path.bezierCurveTo(getReflection(point.x, control.x), getReflection(point.y, control.y), point.x + numbers[j + 0], point.y + numbers[j + 1], point.x + numbers[j + 2], point.y + numbers[j + 3]); control.x = point.x + numbers[j + 0]; control.y = point.y + numbers[j + 1]; point.x += numbers[j + 2]; point.y += numbers[j + 3]; if (j === 0 && doSetFirstPoint === true) firstPoint.copy(point); } break; case 'q': var numbers = parseFloats(data); for (var j = 0, jl = numbers.length; j < jl; j += 4) { path.quadraticCurveTo(point.x + numbers[j + 0], point.y + numbers[j + 1], point.x + numbers[j + 2], point.y + numbers[j + 3]); control.x = point.x + numbers[j + 0]; control.y = point.y + numbers[j + 1]; point.x += numbers[j + 2]; point.y += numbers[j + 3]; if (j === 0 && doSetFirstPoint === true) firstPoint.copy(point); } break; case 't': var numbers = parseFloats(data); for (var j = 0, jl = numbers.length; j < jl; j += 2) { var rx = getReflection(point.x, control.x); var ry = getReflection(point.y, control.y); path.quadraticCurveTo(rx, ry, point.x + numbers[j + 0], point.y + numbers[j + 1]); control.x = rx; control.y = ry; point.x = point.x + numbers[j + 0]; point.y = point.y + numbers[j + 1]; if (j === 0 && doSetFirstPoint === true) firstPoint.copy(point); } break; case 'a': var numbers = parseFloats(data); for (var j = 0, jl = numbers.length; j < jl; j += 7) { var start = point.clone(); point.x += numbers[j + 5]; point.y += numbers[j + 6]; control.x = point.x; control.y = point.y; parseArcCommand(path, numbers[j], numbers[j + 1], numbers[j + 2], numbers[j + 3], numbers[j + 4], start, point); if (j === 0 && doSetFirstPoint === true) firstPoint.copy(point); } break; case 'Z': case 'z': path.currentPath.autoClose = true; if (path.currentPath.curves.length > 0) { // Reset point to beginning of Path point.copy(firstPoint); path.currentPath.currentPoint.copy(point); isFirstPoint = true; } break; default: console.warn(command); } // console.log( type, parseFloats( data ), parseFloats( data ).length ) doSetFirstPoint = false; } return path; } function parseCSSStylesheet(node) { if (!node.sheet || !node.sheet.cssRules || !node.sheet.cssRules.length) return; for (var i = 0; i < node.sheet.cssRules.length; i++) { var stylesheet = node.sheet.cssRules[i]; if (stylesheet.type !== 1) continue; var selectorList = stylesheet.selectorText.split(/,/gm).filter(Boolean).map(function (i) { return i.trim(); }); for (var j = 0; j < selectorList.length; j++) { stylesheets[selectorList[j]] = Object.assign(stylesheets[selectorList[j]] || {}, stylesheet.style); } } } /** * https://www.w3.org/TR/SVG/implnote.html#ArcImplementationNotes * https://mortoray.com/2017/02/16/rendering-an-svg-elliptical-arc-as-bezier-curves/ Appendix: Endpoint to center arc conversion * From * rx ry x-axis-rotation large-arc-flag sweep-flag x y * To * aX, aY, xRadius, yRadius, aStartAngle, aEndAngle, aClockwise, aRotation */ function parseArcCommand(path, rx, ry, x_axis_rotation, large_arc_flag, sweep_flag, start, end) { x_axis_rotation = x_axis_rotation * Math.PI / 180; // Ensure radii are positive rx = Math.abs(rx); ry = Math.abs(ry); // Compute (x1', y1') var dx2 = (start.x - end.x) / 2.0; var dy2 = (start.y - end.y) / 2.0; var x1p = Math.cos(x_axis_rotation) * dx2 + Math.sin(x_axis_rotation) * dy2; var y1p = -Math.sin(x_axis_rotation) * dx2 + Math.cos(x_axis_rotation) * dy2; // Compute (cx', cy') var rxs = rx * rx; var rys = ry * ry; var x1ps = x1p * x1p; var y1ps = y1p * y1p; // Ensure radii are large enough var cr = x1ps / rxs + y1ps / rys; if (cr > 1) { // scale up rx,ry equally so cr == 1 var s = Math.sqrt(cr); rx = s * rx; ry = s * ry; rxs = rx * rx; rys = ry * ry; } var dq = rxs * y1ps + rys * x1ps; var pq = (rxs * rys - dq) / dq; var q = Math.sqrt(Math.max(0, pq)); if (large_arc_flag === sweep_flag) q = -q; var cxp = q * rx * y1p / ry; var cyp = -q * ry * x1p / rx; // Step 3: Compute (cx, cy) from (cx', cy') var cx = Math.cos(x_axis_rotation) * cxp - Math.sin(x_axis_rotation) * cyp + (start.x + end.x) / 2; var cy = Math.sin(x_axis_rotation) * cxp + Math.cos(x_axis_rotation) * cyp + (start.y + end.y) / 2; // Step 4: Compute ��1 and ���� var theta = svgAngle(1, 0, (x1p - cxp) / rx, (y1p - cyp) / ry); var delta = svgAngle((x1p - cxp) / rx, (y1p - cyp) / ry, (-x1p - cxp) / rx, (-y1p - cyp) / ry) % (Math.PI * 2); path.currentPath.absellipse(cx, cy, rx, ry, theta, theta + delta, sweep_flag === 0, x_axis_rotation); } function svgAngle(ux, uy, vx, vy) { var dot = ux * vx + uy * vy; var len = Math.sqrt(ux * ux + uy * uy) * Math.sqrt(vx * vx + vy * vy); var ang = Math.acos(Math.max(-1, Math.min(1, dot / len))); // floating point precision, slightly over values appear if (ux * vy - uy * vx < 0) ang = -ang; return ang; } /* * According to https://www.w3.org/TR/SVG/shapes.html#RectElementRXAttribute * rounded corner should be rendered to elliptical arc, but bezier curve does the job well enough */ function parseRectNode(node) { var x = parseFloatWithUnits(node.getAttribute('x') || 0); var y = parseFloatWithUnits(node.getAttribute('y') || 0); var rx = parseFloatWithUnits(node.getAttribute('rx') || 0); var ry = parseFloatWithUnits(node.getAttribute('ry') || 0); var w = parseFloatWithUnits(node.getAttribute('width')); var h = parseFloatWithUnits(node.getAttribute('height')); var path = new _three.ShapePath(); path.moveTo(x + 2 * rx, y); path.lineTo(x + w - 2 * rx, y); if (rx !== 0 || ry !== 0) path.bezierCurveTo(x + w, y, x + w, y, x + w, y + 2 * ry); path.lineTo(x + w, y + h - 2 * ry); if (rx !== 0 || ry !== 0) path.bezierCurveTo(x + w, y + h, x + w, y + h, x + w - 2 * rx, y + h); path.lineTo(x + 2 * rx, y + h); if (rx !== 0 || ry !== 0) { path.bezierCurveTo(x, y + h, x, y + h, x, y + h - 2 * ry); } path.lineTo(x, y + 2 * ry); if (rx !== 0 || ry !== 0) { path.bezierCurveTo(x, y, x, y, x + 2 * rx, y); } return path; } function parsePolygonNode(node) { function iterator(match, a, b) { var x = parseFloatWithUnits(a); var y = parseFloatWithUnits(b); if (index === 0) { path.moveTo(x, y); } else { path.lineTo(x, y); } index++; } var regex = /(-?[\d\.?]+)[,|\s](-?[\d\.?]+)/g; var path = new _three.ShapePath(); var index = 0; node.getAttribute('points').replace(regex, iterator); path.currentPath.autoClose = true; return path; } function parsePolylineNode(node) { function iterator(match, a, b) { var x = parseFloatWithUnits(a); var y = parseFloatWithUnits(b); if (index === 0) { path.moveTo(x, y); } else { path.lineTo(x, y); } index++; } var regex = /(-?[\d\.?]+)[,|\s](-?[\d\.?]+)/g; var path = new _three.ShapePath(); var index = 0; node.getAttribute('points').replace(regex, iterator); path.currentPath.autoClose = false; return path; } function parseCircleNode(node) { var x = parseFloatWithUnits(node.getAttribute('cx')); var y = parseFloatWithUnits(node.getAttribute('cy')); var r = parseFloatWithUnits(node.getAttribute('r')); var subpath = new _three.Path(); subpath.absarc(x, y, r, 0, Math.PI * 2); var path = new _three.ShapePath(); path.subPaths.push(subpath); return path; } function parseEllipseNode(node) { var x = parseFloatWithUnits(node.getAttribute('cx')); var y = parseFloatWithUnits(node.getAttribute('cy')); var rx = parseFloatWithUnits(node.getAttribute('rx')); var ry = parseFloatWithUnits(node.getAttribute('ry')); var subpath = new _three.Path(); subpath.absellipse(x, y, rx, ry, 0, Math.PI * 2); var path = new _three.ShapePath(); path.subPaths.push(subpath); return path; } function parseLineNode(node) { var x1 = parseFloatWithUnits(node.getAttribute('x1')); var y1 = parseFloatWithUnits(node.getAttribute('y1')); var x2 = parseFloatWithUnits(node.getAttribute('x2')); var y2 = parseFloatWithUnits(node.getAttribute('y2')); var path = new _three.ShapePath(); path.moveTo(x1, y1); path.lineTo(x2, y2); path.currentPath.autoClose = false; return path; } // function parseStyle(node, style) { style = Object.assign({}, style); // clone style var stylesheetStyles = {}; if (node.hasAttribute('class')) { var classSelectors = node.getAttribute('class').split(/\s/).filter(Boolean).map(function (i) { return i.trim(); }); for (var i = 0; i < classSelectors.length; i++) { stylesheetStyles = Object.assign(stylesheetStyles, stylesheets['.' + classSelectors[i]]); } } if (node.hasAttribute('id')) { stylesheetStyles = Object.assign(stylesheetStyles, stylesheets['#' + node.getAttribute('id')]); } function addStyle(svgName, jsName, adjustFunction) { if (adjustFunction === undefined) adjustFunction = function copy(v) { if (v.startsWith('url')) console.warn('SVGLoader: url access in attributes is not implemented.'); return v; }; if (node.hasAttribute(svgName)) style[jsName] = adjustFunction(node.getAttribute(svgName)); if (stylesheetStyles[svgName]) style[jsName] = adjustFunction(stylesheetStyles[svgName]); if (node.style && node.style[svgName] !== '') style[jsName] = adjustFunction(node.style[svgName]); } function clamp(v) { return Math.max(0, Math.min(1, parseFloatWithUnits(v))); } function positive(v) { return Math.max(0, parseFloatWithUnits(v)); } addStyle('fill', 'fill'); addStyle('fill-opacity', 'fillOpacity', clamp); addStyle('opacity', 'opacity', clamp); addStyle('stroke', 'stroke'); addStyle('stroke-opacity', 'strokeOpacity', clamp); addStyle('stroke-width', 'strokeWidth', positive); addStyle('stroke-linejoin', 'strokeLineJoin'); addStyle('stroke-linecap', 'strokeLineCap'); addStyle('stroke-miterlimit', 'strokeMiterLimit', positive); addStyle('visibility', 'visibility'); return style; } // http://www.w3.org/TR/SVG11/implnote.html#PathElementImplementationNotes function getReflection(a, b) { return a - (b - a); } function parseFloats(string) { var array = string.split(/[\s,]+|(?=\s?[+\-])/); for (var i = 0; i < array.length; i++) { var number = array[i]; // Handle values like 48.6037.7.8 // TODO Find a regex for this if (number.indexOf('.') !== number.lastIndexOf('.')) { var split = number.split('.'); for (var s = 2; s < split.length; s++) { array.splice(i + s - 1, 0, '0.' + split[s]); } } array[i] = parseFloatWithUnits(number); } return array; } // Units var units = ['mm', 'cm', 'in', 'pt', 'pc', 'px']; // Conversion: [ fromUnit ][ toUnit ] (-1 means dpi dependent) var unitConversion = { mm: { mm: 1, cm: 0.1, "in": 1 / 25.4, pt: 72 / 25.4, pc: 6 / 25.4, px: -1 }, cm: { mm: 10, cm: 1, "in": 1 / 2.54, pt: 72 / 2.54, pc: 6 / 2.54, px: -1 }, "in": { mm: 25.4, cm: 2.54, "in": 1, pt: 72, pc: 6, px: -1 }, pt: { mm: 25.4 / 72, cm: 2.54 / 72, "in": 1 / 72, pt: 1, pc: 6 / 72, px: -1 }, pc: { mm: 25.4 / 6, cm: 2.54 / 6, "in": 1 / 6, pt: 72 / 6, pc: 1, px: -1 }, px: { px: 1 } }; function parseFloatWithUnits(string) { var theUnit = 'px'; if (typeof string === 'string' || string instanceof String) { for (var i = 0, n = units.length; i < n; i++) { var u = units[i]; if (string.endsWith(u)) { theUnit = u; string = string.substring(0, string.length - u.length); break; } } } var scale = undefined; if (theUnit === 'px' && scope.defaultUnit !== 'px') { // Conversion scale from pixels to inches, then to default units scale = unitConversion['in'][scope.defaultUnit] / scope.defaultDPI; } else { scale = unitConversion[theUnit][scope.defaultUnit]; if (scale < 0) { // Conversion scale to pixels scale = unitConversion[theUnit]['in'] * scope.defaultDPI; } } return scale * parseFloat(string); } // Transforms function getNodeTransform(node) { if (!(node.hasAttribute('transform') || node.nodeName === 'use' && (node.hasAttribute('x') || node.hasAttribute('y')))) { return null; } var transform = parseNodeTransform(node); if (transformStack.length > 0) { transform.premultiply(transformStack[transformStack.length - 1]); } currentTransform.copy(transform); transformStack.push(transform); return transform; } function parseNodeTransform(node) { var transform = new _three.Matrix3(); var currentTransform = tempTransform0; if (node.nodeName === 'use' && (node.hasAttribute('x') || node.hasAttribute('y'))) { var tx = parseFloatWithUnits(node.getAttribute('x')); var ty = parseFloatWithUnits(node.getAttribute('y')); transform.translate(tx, ty); } if (node.hasAttribute('transform')) { var transformsTexts = node.getAttribute('transform').split(')'); for (var tIndex = transformsTexts.length - 1; tIndex >= 0; tIndex--) { var transformText = transformsTexts[tIndex].trim(); if (transformText === '') continue; var openParPos = transformText.indexOf('('); var closeParPos = transformText.length; if (openParPos > 0 && openParPos < closeParPos) { var transformType = transformText.substr(0, openParPos); var array = parseFloats(transformText.substr(openParPos + 1, closeParPos - openParPos - 1)); currentTransform.identity(); switch (transformType) { case 'translate': if (array.length >= 1) { var tx = array[0]; var ty = tx; if (array.length >= 2) { ty = array[1]; } currentTransform.translate(tx, ty); } break; case 'rotate': if (array.length >= 1) { var angle = 0; var cx = 0; var cy = 0; // Angle angle = -array[0] * Math.PI / 180; if (array.length >= 3) { // Center x, y cx = array[1]; cy = array[2]; } // Rotate around center (cx, cy) tempTransform1.identity().translate(-cx, -cy); tempTransform2.identity().rotate(angle); tempTransform3.multiplyMatrices(tempTransform2, tempTransform1); tempTransform1.identity().translate(cx, cy); currentTransform.multiplyMatrices(tempTransform1, tempTransform3); } break; case 'scale': if (array.length >= 1) { var scaleX = array[0]; var scaleY = scaleX; if (array.length >= 2) { scaleY = array[1]; } currentTransform.scale(scaleX, scaleY); } break; case 'skewX': if (array.length === 1) { currentTransform.set(1, Math.tan(array[0] * Math.PI / 180), 0, 0, 1, 0, 0, 0, 1); } break; case 'skewY': if (array.length === 1) { currentTransform.set(1, 0, 0, Math.tan(array[0] * Math.PI / 180), 1, 0, 0, 0, 1); } break; case 'matrix': if (array.length === 6) { currentTransform.set(array[0], array[2], array[4], array[1], array[3], array[5], 0, 0, 1); } break; } } transform.premultiply(currentTransform); } } return transform; } function transformPath(path, m) { function transfVec2(v2) { tempV3.set(v2.x, v2.y, 1).applyMatrix3(m); v2.set(tempV3.x, tempV3.y); } var isRotated = isTransformRotated(m); var subPaths = path.subPaths; for (var i = 0, n = subPaths.length; i < n; i++) { var subPath = subPaths[i]; var curves = subPath.curves; for (var j = 0; j < curves.length; j++) { var curve = curves[j]; if (curve.isLineCurve) { transfVec2(curve.v1); transfVec2(curve.v2); } else if (curve.isCubicBezierCurve) { transfVec2(curve.v0); transfVec2(curve.v1); transfVec2(curve.v2); transfVec2(curve.v3); } else if (curve.isQuadraticBezierCurve) { transfVec2(curve.v0); transfVec2(curve.v1); transfVec2(curve.v2); } else if (curve.isEllipseCurve) { if (isRotated) { console.warn('SVGLoader: Elliptic arc or ellipse rotation or skewing is not implemented.'); } tempV2.set(curve.aX, curve.aY); transfVec2(tempV2); curve.aX = tempV2.x; curve.aY = tempV2.y; curve.xRadius *= getTransformScaleX(m); curve.yRadius *= getTransformScaleY(m); } } } } function isTransformRotated(m) { return m.elements[1] !== 0 || m.elements[3] !== 0; } function getTransformScaleX(m) { var te = m.elements; return Math.sqrt(te[0] * te[0] + te[1] * te[1]); } function getTransformScaleY(m) { var te = m.elements; return Math.sqrt(te[3] * te[3] + te[4] * te[4]); } // var paths = []; var stylesheets = {}; var transformStack = []; var tempTransform0 = new _three.Matrix3(); var tempTransform1 = new _three.Matrix3(); var tempTransform2 = new _three.Matrix3(); var tempTransform3 = new _three.Matrix3(); var tempV2 = new _three.Vector2(); var tempV3 = new _three.Vector3(); var currentTransform = new _three.Matrix3(); var xml = new DOMParser().parseFromString(text, 'image/svg+xml'); // application/xml parseNode(xml.documentElement, { fill: '#000', fillOpacity: 1, strokeOpacity: 1, strokeWidth: 1, strokeLineJoin: 'miter', strokeLineCap: 'butt', strokeMiterLimit: 4 }); var data = { paths: paths, xml: xml.documentElement }; // console.log( paths ); return data; } }); SVGLoader.getStrokeStyle = function (width, color, lineJoin, lineCap, miterLimit) { // Param width: Stroke width // Param color: As returned by THREE.Color.getStyle() // Param lineJoin: One of "round", "bevel", "miter" or "miter-limit" // Param lineCap: One of "round", "square" or "butt" // Param miterLimit: Maximum join length, in multiples of the "width" parameter (join is truncated if it exceeds that distance) // Returns style object width = width !== undefined ? width : 1; color = color !== undefined ? color : '#000'; lineJoin = lineJoin !== undefined ? lineJoin : 'miter'; lineCap = lineCap !== undefined ? lineCap : 'butt'; miterLimit = miterLimit !== undefined ? miterLimit : 4; return { strokeColor: color, strokeWidth: width, strokeLineJoin: lineJoin, strokeLineCap: lineCap, strokeMiterLimit: miterLimit }; }; SVGLoader.pointsToStroke = function (points, style, arcDivisions, minDistance) { // Generates a stroke with some witdh around the given path. // The path can be open or closed (last point equals to first point) // Param points: Array of Vector2D (the path). Minimum 2 points. // Param style: Object with SVG properties as returned by SVGLoader.getStrokeStyle(), or SVGLoader.parse() in the path.userData.style object // Params arcDivisions: Arc divisions for round joins and endcaps. (Optional) // Param minDistance: Points closer to this distance will be merged. (Optional) // Returns BufferGeometry with stroke triangles (In plane z = 0). UV coordinates are generated ('u' along path. 'v' across it, from left to right) var vertices = []; var normals = []; var uvs = []; if (SVGLoader.pointsToStrokeWithBuffers(points, style, arcDivisions, minDistance, vertices, normals, uvs) === 0) { return null; } var geometry = new _three.BufferGeometry(); geometry.setAttribute('position', new _three.Float32BufferAttribute(vertices, 3)); geometry.setAttribute('normal', new _three.Float32BufferAttribute(normals, 3)); geometry.setAttribute('uv', new _three.Float32BufferAttribute(uvs, 2)); return geometry; }; SVGLoader.pointsToStrokeWithBuffers = function () { var tempV2_1 = new _three.Vector2(); var tempV2_2 = new _three.Vector2(); var tempV2_3 = new _three.Vector2(); var tempV2_4 = new _three.Vector2(); var tempV2_5 = new _three.Vector2(); var tempV2_6 = new _three.Vector2(); var tempV2_7 = new _three.Vector2(); var lastPointL = new _three.Vector2(); var lastPointR = new _three.Vector2(); var point0L = new _three.Vector2(); var point0R = new _three.Vector2(); var currentPointL = new _three.Vector2(); var currentPointR = new _three.Vector2(); var nextPointL = new _three.Vector2(); var nextPointR = new _three.Vector2(); var innerPoint = new _three.Vector2(); var outerPoint = new _three.Vector2(); return function (points, style, arcDivisions, minDistance, vertices, normals, uvs, vertexOffset) { // This function can be called to update existing arrays or buffers. // Accepts same parameters as pointsToStroke, plus the buffers and optional offset. // Param vertexOffset: Offset vertices to start writing in the buffers (3 elements/vertex for vertices and normals, and 2 elements/vertex for uvs) // Returns number of written vertices / normals / uvs pairs // if 'vertices' parameter is undefined no triangles will be generated, but the returned vertices count will still be valid (useful to preallocate the buffers) // 'normals' and 'uvs' buffers are optional arcDivisions = arcDivisions !== undefined ? arcDivisions : 12; minDistance = minDistance !== undefined ? minDistance : 0.001; vertexOffset = vertexOffset !== undefined ? vertexOffset : 0; // First ensure there are no duplicated points points = removeDuplicatedPoints(points); var numPoints = points.length; if (numPoints < 2) return 0; var isClosed = points[0].equals(points[numPoints - 1]); var currentPoint; var previousPoint = points[0]; var nextPoint; var strokeWidth2 = style.strokeWidth / 2; var deltaU = 1 / (numPoints - 1); var u0 = 0; var innerSideModified; var joinIsOnLeftSide; var isMiter; var initialJoinIsOnLeftSide = false; var numVertices = 0; var currentCoordinate = vertexOffset * 3; var currentCoordinateUV = vertexOffset * 2; // Get initial left and right stroke points getNormal(points[0], points[1], tempV2_1).multiplyScalar(strokeWidth2); lastPointL.copy(points[0]).sub(tempV2_1); lastPointR.copy(points[0]).add(tempV2_1); point0L.copy(lastPointL); point0R.copy(lastPointR); for (var iPoint = 1; iPoint < numPoints; iPoint++) { currentPoint = points[iPoint]; // Get next point if (iPoint === numPoints - 1) { if (isClosed) { // Skip duplicated initial point nextPoint = points[1]; } else nextPoint = undefined; } else { nextPoint = points[iPoint + 1]; } // Normal of previous segment in tempV2_1 var normal1 = tempV2_1; getNormal(previousPoint, currentPoint, normal1); tempV2_3.copy(normal1).multiplyScalar(strokeWidth2); currentPointL.copy(currentPoint).sub(tempV2_3); currentPointR.copy(currentPoint).add(tempV2_3); var u1 = u0 + deltaU; innerSideModified = false; if (nextPoint !== undefined) { // Normal of next segment in tempV2_2 getNormal(currentPoint, nextPoint, tempV2_2); tempV2_3.copy(tempV2_2).multiplyScalar(strokeWidth2); nextPointL.copy(currentPoint).sub(tempV2_3); nextPointR.copy(currentPoint).add(tempV2_3); joinIsOnLeftSide = true; tempV2_3.subVectors(nextPoint, previousPoint); if (normal1.dot(tempV2_3) < 0) { joinIsOnLeftSide = false; } if (iPoint === 1) initialJoinIsOnLeftSide = joinIsOnLeftSide; tempV2_3.subVectors(nextPoint, currentPoint); tempV2_3.normalize(); var dot = Math.abs(normal1.dot(tempV2_3)); // If path is straight, don't create join if (dot !== 0) { // Compute inner and outer segment intersections var miterSide = strokeWidth2 / dot; tempV2_3.multiplyScalar(-miterSide); tempV2_4.subVectors(currentPoint, previousPoint); tempV2_5.copy(tempV2_4).setLength(miterSide).add(tempV2_3); innerPoint.copy(tempV2_5).negate(); var miterLength2 = tempV2_5.length(); var segmentLengthPrev = tempV2_4.length(); tempV2_4.divideScalar(segmentLengthPrev); tempV2_6.subVectors(nextPoint, currentPoint); var segmentLengthNext = tempV2_6.length(); tempV2_6.divideScalar(segmentLengthNext); // Check that previous and next segments doesn't overlap with the innerPoint of intersection if (tempV2_4.dot(innerPoint) < segmentLengthPrev && tempV2_6.dot(innerPoint) < segmentLengthNext) { innerSideModified = true; } outerPoint.copy(tempV2_5).add(currentPoint); innerPoint.add(currentPoint); isMiter = false; if (innerSideModified) { if (joinIsOnLeftSide) { nextPointR.copy(innerPoint); currentPointR.copy(innerPoint); } else { nextPointL.copy(innerPoint); currentPointL.copy(innerPoint); } } else { // The segment triangles are generated here if there was overlapping makeSegmentTriangles(); } switch (style.strokeLineJoin) { case 'bevel': makeSegmentWithBevelJoin(joinIsOnLeftSide, innerSideModified, u1); break; case 'round': // Segment triangles createSegmentTrianglesWithMiddleSection(joinIsOnLeftSide, innerSideModified); // Join triangles if (joinIsOnLeftSide) { makeCircularSector(currentPoint, currentPointL, nextPointL, u1, 0); } else { makeCircularSector(currentPoint, nextPointR, currentPointR, u1, 1); } break; case 'miter': case 'miter-clip': default: var miterFraction = strokeWidth2 * style.strokeMiterLimit / miterLength2; if (miterFraction < 1) { // The join miter length exceeds the miter limit if (style.strokeLineJoin !== 'miter-clip') { makeSegmentWithBevelJoin(joinIsOnLeftSide, innerSideModified, u1); break; } else { // Segment triangles createSegmentTrianglesWithMiddleSection(joinIsOnLeftSide, innerSideModified); // Miter-clip join triangles if (joinIsOnLeftSide) { tempV2_6.subVectors(outerPoint, currentPointL).multiplyScalar(miterFraction).add(currentPointL); tempV2_7.subVectors(outerPoint, nextPointL).multiplyScalar(miterFraction).add(nextPointL); addVertex(currentPointL, u1, 0); addVertex(tempV2_6, u1, 0); addVertex(currentPoint, u1, 0.5); addVertex(currentPoint, u1, 0.5); addVertex(tempV2_6, u1, 0); addVertex(tempV2_7, u1, 0); addVertex(currentPoint, u1, 0.5); addVertex(tempV2_7, u1, 0); addVertex(nextPointL, u1, 0); } else { tempV2_6.subVectors(outerPoint, currentPointR).multiplyScalar(miterFraction).add(currentPointR); tempV2_7.subVectors(outerPoint, nextPointR).multiplyScalar(miterFraction).add(nextPointR); addVertex(currentPointR, u1, 1); addVertex(tempV2_6, u1, 1); addVertex(currentPoint, u1, 0.5); addVertex(currentPoint, u1, 0.5); addVertex(tempV2_6, u1, 1); addVertex(tempV2_7, u1, 1); addVertex(currentPoint, u1, 0.5); addVertex(tempV2_7, u1, 1); addVertex(nextPointR, u1, 1); } } } else { // Miter join segment triangles if (innerSideModified) { // Optimized segment + join triangles if (joinIsOnLeftSide) { addVertex(lastPointR, u0, 1); addVertex(lastPointL, u0, 0); addVertex(outerPoint, u1, 0); addVertex(lastPointR, u0, 1); addVertex(outerPoint, u1, 0); addVertex(innerPoint, u1, 1); } else { addVertex(lastPointR, u0, 1); addVertex(lastPointL, u0, 0); addVertex(outerPoint, u1, 1); addVertex(lastPointL, u0, 0); addVertex(innerPoint, u1, 0); addVertex(outerPoint, u1, 1); } if (joinIsOnLeftSide) { nextPointL.copy(outerPoint); } else { nextPointR.copy(outerPoint); } } else { // Add extra miter join triangles if (joinIsOnLeftSide) { addVertex(currentPointL, u1, 0); addVertex(outerPoint, u1, 0); addVertex(currentPoint, u1, 0.5); addVertex(currentPoint, u1, 0.5); addVertex(outerPoint, u1, 0); addVertex(nextPointL, u1, 0); } else { addVertex(currentPointR, u1, 1); addVertex(outerPoint, u1, 1); addVertex(currentPoint, u1, 0.5); addVertex(currentPoint, u1, 0.5); addVertex(outerPoint, u1, 1); addVertex(nextPointR, u1, 1); } } isMiter = true; } break; } } else { // The segment triangles are generated here when two consecutive points are collinear makeSegmentTriangles(); } } else { // The segment triangles are generated here if it is the ending segment makeSegmentTriangles(); } if (!isClosed && iPoint === numPoints - 1) { // Start line endcap addCapGeometry(points[0], point0L, point0R, joinIsOnLeftSide, true, u0); } // Increment loop variables u0 = u1; previousPoint = currentPoint; lastPointL.copy(nextPointL); lastPointR.copy(nextPointR); } if (!isClosed) { // Ending line endcap addCapGeometry(currentPoint, currentPointL, currentPointR, joinIsOnLeftSide, false, u1); } else if (innerSideModified && vertices) { // Modify path first segment vertices to adjust to the segments inner and outer intersections var lastOuter = outerPoint; var lastInner = innerPoint; if (initialJoinIsOnLeftSide !== joinIsOnLeftSide) { lastOuter = innerPoint; lastInner = outerPoint; } if (joinIsOnLeftSide) { if (isMiter || initialJoinIsOnLeftSide) { lastInner.toArray(vertices, 0 * 3); lastInner.toArray(vertices, 3 * 3); if (isMiter) { lastOuter.toArray(vertices, 1 * 3); } } } else { if (isMiter || !initialJoinIsOnLeftSide) { lastInner.toArray(vertices, 1 * 3); lastInner.toArray(vertices, 3 * 3); if (isMiter) { lastOuter.toArray(vertices, 0 * 3); } } } } return numVertices; // -- End of algorithm // -- Functions function getNormal(p1, p2, result) { result.subVectors(p2, p1); return result.set(-result.y, result.x).normalize(); } function addVertex(position, u, v) { if (vertices) { vertices[currentCoordinate] = position.x; vertices[currentCoordinate + 1] = position.y; vertices[currentCoordinate + 2] = 0; if (normals) { normals[currentCoordinate] = 0; normals[currentCoordinate + 1] = 0; normals[currentCoordinate + 2] = 1; } currentCoordinate += 3; if (uvs) { uvs[currentCoordinateUV] = u; uvs[currentCoordinateUV + 1] = v; currentCoordinateUV += 2; } } numVertices += 3; } function makeCircularSector(center, p1, p2, u, v) { // param p1, p2: Points in the circle arc. // p1 and p2 are in clockwise direction. tempV2_1.copy(p1).sub(center).normalize(); tempV2_2.copy(p2).sub(center).normalize(); var angle = Math.PI; var dot = tempV2_1.dot(tempV2_2); if (Math.abs(dot) < 1) angle = Math.abs(Math.acos(dot)); angle /= arcDivisions; tempV2_3.copy(p1); for (var i = 0, il = arcDivisions - 1; i < il; i++) { tempV2_4.copy(tempV2_3).rotateAround(center, angle); addVertex(tempV2_3, u, v); addVertex(tempV2_4, u, v); addVertex(center, u, 0.5); tempV2_3.copy(tempV2_4); } addVertex(tempV2_4, u, v); addVertex(p2, u, v); addVertex(center, u, 0.5); } function makeSegmentTriangles() { addVertex(lastPointR, u0, 1); addVertex(lastPointL, u0, 0); addVertex(currentPointL, u1, 0); addVertex(lastPointR, u0, 1); addVertex(currentPointL, u1, 1); addVertex(currentPointR, u1, 0); } function makeSegmentWithBevelJoin(joinIsOnLeftSide, innerSideModified, u) { if (innerSideModified) { // Optimized segment + bevel triangles if (joinIsOnLeftSide) { // Path segments triangles addVertex(lastPointR, u0, 1); addVertex(lastPointL, u0, 0); addVertex(currentPointL, u1, 0); addVertex(lastPointR, u0, 1); addVertex(currentPointL, u1, 0); addVertex(innerPoint, u1, 1); // Bevel join triangle addVertex(currentPointL, u, 0); addVertex(nextPointL, u, 0); addVertex(innerPoint, u, 0.5); } else { // Path segments triangles addVertex(lastPointR, u0, 1); addVertex(lastPointL, u0, 0); addVertex(currentPointR, u1, 1); addVertex(lastPointL, u0, 0); addVertex(innerPoint, u1, 0); addVertex(currentPointR, u1, 1); // Bevel join triangle addVertex(currentPointR, u, 1); addVertex(nextPointR, u, 0); addVertex(innerPoint, u, 0.5); } } else { // Bevel join triangle. The segment triangles are done in the main loop if (joinIsOnLeftSide) { addVertex(currentPointL, u, 0); addVertex(nextPointL, u, 0); addVertex(currentPoint, u, 0.5); } else { addVertex(currentPointR, u, 1); addVertex(nextPointR, u, 0); addVertex(currentPoint, u, 0.5); } } } function createSegmentTrianglesWithMiddleSection(joinIsOnLeftSide, innerSideModified) { if (innerSideModified) { if (joinIsOnLeftSide) { addVertex(lastPointR, u0, 1); addVertex(lastPointL, u0, 0); addVertex(currentPointL, u1, 0); addVertex(lastPointR, u0, 1); addVertex(currentPointL, u1, 0); addVertex(innerPoint, u1, 1); addVertex(currentPointL, u0, 0); addVertex(currentPoint, u1, 0.5); addVertex(innerPoint, u1, 1); addVertex(currentPoint, u1, 0.5); addVertex(nextPointL, u0, 0); addVertex(innerPoint, u1, 1); } else { addVertex(lastPointR, u0, 1);