UNPKG

three-stdlib

Version:

stand-alone library of threejs examples

1,340 lines (1,339 loc) 69.5 kB
import { Loader, FileLoader, Matrix3, Vector2, Vector3, ShapeUtils, Box2, Shape, Path, BufferGeometry, Float32BufferAttribute, ShapePath } from "three"; const COLOR_SPACE_SVG = "srgb"; const SVGLoader = /* @__PURE__ */ (() => { class SVGLoader2 extends Loader { constructor(manager) { super(manager); this.defaultDPI = 90; this.defaultUnit = "px"; } load(url, onLoad, onProgress, onError) { const scope = this; const loader = new 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(text) { const scope = this; function parseNode(node, style) { if (node.nodeType !== 1) return; const transform = getNodeTransform(node); let isDefsNode = false; let path = null; switch (node.nodeName) { case "svg": style = parseStyle(node, style); 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": isDefsNode = true; break; case "use": style = parseStyle(node, style); const href = node.getAttributeNS("http://www.w3.org/1999/xlink", "href") || ""; const usedNodeId = href.substring(1); const usedNode = node.viewportElement.getElementById(usedNodeId); if (usedNode) { parseNode(usedNode, style); } else { console.warn("SVGLoader: 'use node' references non-existent node id: " + usedNodeId); } break; } if (path) { if (style.fill !== void 0 && style.fill !== "none") { path.color.setStyle(style.fill, COLOR_SPACE_SVG); } transformPath(path, currentTransform); paths.push(path); path.userData = { node, style }; } const childNodes = node.childNodes; for (let i = 0; i < childNodes.length; i++) { const node2 = childNodes[i]; if (isDefsNode && node2.nodeName !== "style" && node2.nodeName !== "defs") { continue; } parseNode(node2, style); } if (transform) { transformStack.pop(); if (transformStack.length > 0) { currentTransform.copy(transformStack[transformStack.length - 1]); } else { currentTransform.identity(); } } } function parsePathNode(node) { const path = new ShapePath(); const point = new Vector2(); const control = new Vector2(); const firstPoint = new Vector2(); let isFirstPoint = true; let doSetFirstPoint = false; const d = node.getAttribute("d"); if (d === "" || d === "none") return null; const commands = d.match(/[a-df-z][^a-df-z]*/gi); for (let i = 0, l = commands.length; i < l; i++) { const command = commands[i]; const type = command.charAt(0); const data2 = command.slice(1).trim(); if (isFirstPoint === true) { doSetFirstPoint = true; isFirstPoint = false; } let numbers; switch (type) { case "M": numbers = parseFloats(data2); for (let 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) firstPoint.copy(point); } break; case "H": numbers = parseFloats(data2); for (let 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": numbers = parseFloats(data2); for (let 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": numbers = parseFloats(data2); for (let 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": numbers = parseFloats(data2); for (let 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": numbers = parseFloats(data2); for (let 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": numbers = parseFloats(data2); for (let 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": numbers = parseFloats(data2); for (let j = 0, jl = numbers.length; j < jl; j += 2) { const rx = getReflection(point.x, control.x); const 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": numbers = parseFloats(data2, [3, 4], 7); for (let j = 0, jl = numbers.length; j < jl; j += 7) { if (numbers[j + 5] == point.x && numbers[j + 6] == point.y) continue; const 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": numbers = parseFloats(data2); for (let 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) firstPoint.copy(point); } break; case "h": numbers = parseFloats(data2); for (let 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": numbers = parseFloats(data2); for (let 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": numbers = parseFloats(data2); for (let 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": numbers = parseFloats(data2); for (let 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": numbers = parseFloats(data2); for (let 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": numbers = parseFloats(data2); for (let 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": numbers = parseFloats(data2); for (let j = 0, jl = numbers.length; j < jl; j += 2) { const rx = getReflection(point.x, control.x); const 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": numbers = parseFloats(data2, [3, 4], 7); for (let j = 0, jl = numbers.length; j < jl; j += 7) { if (numbers[j + 5] == 0 && numbers[j + 6] == 0) continue; const 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) { point.copy(firstPoint); path.currentPath.currentPoint.copy(point); isFirstPoint = true; } break; default: console.warn(command); } doSetFirstPoint = false; } return path; } function parseCSSStylesheet(node) { if (!node.sheet || !node.sheet.cssRules || !node.sheet.cssRules.length) return; for (let i = 0; i < node.sheet.cssRules.length; i++) { const stylesheet = node.sheet.cssRules[i]; if (stylesheet.type !== 1) continue; const selectorList = stylesheet.selectorText.split(/,/gm).filter(Boolean).map((i2) => i2.trim()); for (let j = 0; j < selectorList.length; j++) { const definitions = Object.fromEntries(Object.entries(stylesheet.style).filter(([, v]) => v !== "")); stylesheets[selectorList[j]] = Object.assign(stylesheets[selectorList[j]] || {}, definitions); } } } function parseArcCommand(path, rx, ry, x_axis_rotation, large_arc_flag, sweep_flag, start, end) { if (rx == 0 || ry == 0) { path.lineTo(end.x, end.y); return; } x_axis_rotation = x_axis_rotation * Math.PI / 180; rx = Math.abs(rx); ry = Math.abs(ry); const dx2 = (start.x - end.x) / 2; const dy2 = (start.y - end.y) / 2; const x1p = Math.cos(x_axis_rotation) * dx2 + Math.sin(x_axis_rotation) * dy2; const y1p = -Math.sin(x_axis_rotation) * dx2 + Math.cos(x_axis_rotation) * dy2; let rxs = rx * rx; let rys = ry * ry; const x1ps = x1p * x1p; const y1ps = y1p * y1p; const cr = x1ps / rxs + y1ps / rys; if (cr > 1) { const s = Math.sqrt(cr); rx = s * rx; ry = s * ry; rxs = rx * rx; rys = ry * ry; } const dq = rxs * y1ps + rys * x1ps; const pq = (rxs * rys - dq) / dq; let q = Math.sqrt(Math.max(0, pq)); if (large_arc_flag === sweep_flag) q = -q; const cxp = q * rx * y1p / ry; const cyp = -q * ry * x1p / rx; const cx = Math.cos(x_axis_rotation) * cxp - Math.sin(x_axis_rotation) * cyp + (start.x + end.x) / 2; const cy = Math.sin(x_axis_rotation) * cxp + Math.cos(x_axis_rotation) * cyp + (start.y + end.y) / 2; const theta = svgAngle(1, 0, (x1p - cxp) / rx, (y1p - cyp) / ry); const 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) { const dot = ux * vx + uy * vy; const len = Math.sqrt(ux * ux + uy * uy) * Math.sqrt(vx * vx + vy * vy); let ang = Math.acos(Math.max(-1, Math.min(1, dot / len))); if (ux * vy - uy * vx < 0) ang = -ang; return ang; } function parseRectNode(node) { const x = parseFloatWithUnits(node.getAttribute("x") || 0); const y = parseFloatWithUnits(node.getAttribute("y") || 0); const rx = parseFloatWithUnits(node.getAttribute("rx") || node.getAttribute("ry") || 0); const ry = parseFloatWithUnits(node.getAttribute("ry") || node.getAttribute("rx") || 0); const w = parseFloatWithUnits(node.getAttribute("width")); const h = parseFloatWithUnits(node.getAttribute("height")); const bci = 1 - 0.551915024494; const path = new ShapePath(); path.moveTo(x + rx, y); path.lineTo(x + w - rx, y); if (rx !== 0 || ry !== 0) { path.bezierCurveTo(x + w - rx * bci, y, x + w, y + ry * bci, x + w, y + ry); } path.lineTo(x + w, y + h - ry); if (rx !== 0 || ry !== 0) { path.bezierCurveTo(x + w, y + h - ry * bci, x + w - rx * bci, y + h, x + w - rx, y + h); } path.lineTo(x + rx, y + h); if (rx !== 0 || ry !== 0) { path.bezierCurveTo(x + rx * bci, y + h, x, y + h - ry * bci, x, y + h - ry); } path.lineTo(x, y + ry); if (rx !== 0 || ry !== 0) { path.bezierCurveTo(x, y + ry * bci, x + rx * bci, y, x + rx, y); } return path; } function parsePolygonNode(node) { function iterator(match, a, b) { const x = parseFloatWithUnits(a); const y = parseFloatWithUnits(b); if (index === 0) { path.moveTo(x, y); } else { path.lineTo(x, y); } index++; } const regex = /([+-]?\d*\.?\d+(?:e[+-]?\d+)?)(?:,|\s)([+-]?\d*\.?\d+(?:e[+-]?\d+)?)/g; const path = new ShapePath(); let index = 0; node.getAttribute("points").replace(regex, iterator); path.currentPath.autoClose = true; return path; } function parsePolylineNode(node) { function iterator(match, a, b) { const x = parseFloatWithUnits(a); const y = parseFloatWithUnits(b); if (index === 0) { path.moveTo(x, y); } else { path.lineTo(x, y); } index++; } const regex = /([+-]?\d*\.?\d+(?:e[+-]?\d+)?)(?:,|\s)([+-]?\d*\.?\d+(?:e[+-]?\d+)?)/g; const path = new ShapePath(); let index = 0; node.getAttribute("points").replace(regex, iterator); path.currentPath.autoClose = false; return path; } function parseCircleNode(node) { const x = parseFloatWithUnits(node.getAttribute("cx") || 0); const y = parseFloatWithUnits(node.getAttribute("cy") || 0); const r = parseFloatWithUnits(node.getAttribute("r") || 0); const subpath = new Path(); subpath.absarc(x, y, r, 0, Math.PI * 2); const path = new ShapePath(); path.subPaths.push(subpath); return path; } function parseEllipseNode(node) { const x = parseFloatWithUnits(node.getAttribute("cx") || 0); const y = parseFloatWithUnits(node.getAttribute("cy") || 0); const rx = parseFloatWithUnits(node.getAttribute("rx") || 0); const ry = parseFloatWithUnits(node.getAttribute("ry") || 0); const subpath = new Path(); subpath.absellipse(x, y, rx, ry, 0, Math.PI * 2); const path = new ShapePath(); path.subPaths.push(subpath); return path; } function parseLineNode(node) { const x1 = parseFloatWithUnits(node.getAttribute("x1") || 0); const y1 = parseFloatWithUnits(node.getAttribute("y1") || 0); const x2 = parseFloatWithUnits(node.getAttribute("x2") || 0); const y2 = parseFloatWithUnits(node.getAttribute("y2") || 0); const path = new ShapePath(); path.moveTo(x1, y1); path.lineTo(x2, y2); path.currentPath.autoClose = false; return path; } function parseStyle(node, style) { style = Object.assign({}, style); let stylesheetStyles = {}; if (node.hasAttribute("class")) { const classSelectors = node.getAttribute("class").split(/\s/).filter(Boolean).map((i) => i.trim()); for (let 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 === void 0) 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("fill-rule", "fillRule"); 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; } function getReflection(a, b) { return a - (b - a); } function parseFloats(input, flags, stride) { if (typeof input !== "string") { throw new TypeError("Invalid input: " + typeof input); } const RE = { SEPARATOR: /[ \t\r\n\,.\-+]/, WHITESPACE: /[ \t\r\n]/, DIGIT: /[\d]/, SIGN: /[-+]/, POINT: /\./, COMMA: /,/, EXP: /e/i, FLAGS: /[01]/ }; const SEP = 0; const INT = 1; const FLOAT = 2; const EXP = 3; let state = SEP; let seenComma = true; let number = "", exponent = ""; const result = []; function throwSyntaxError(current2, i, partial) { const error = new SyntaxError('Unexpected character "' + current2 + '" at index ' + i + "."); error.partial = partial; throw error; } function newNumber() { if (number !== "") { if (exponent === "") result.push(Number(number)); else result.push(Number(number) * Math.pow(10, Number(exponent))); } number = ""; exponent = ""; } let current; const length = input.length; for (let i = 0; i < length; i++) { current = input[i]; if (Array.isArray(flags) && flags.includes(result.length % stride) && RE.FLAGS.test(current)) { state = INT; number = current; newNumber(); continue; } if (state === SEP) { if (RE.WHITESPACE.test(current)) { continue; } if (RE.DIGIT.test(current) || RE.SIGN.test(current)) { state = INT; number = current; continue; } if (RE.POINT.test(current)) { state = FLOAT; number = current; continue; } if (RE.COMMA.test(current)) { if (seenComma) { throwSyntaxError(current, i, result); } seenComma = true; } } if (state === INT) { if (RE.DIGIT.test(current)) { number += current; continue; } if (RE.POINT.test(current)) { number += current; state = FLOAT; continue; } if (RE.EXP.test(current)) { state = EXP; continue; } if (RE.SIGN.test(current) && number.length === 1 && RE.SIGN.test(number[0])) { throwSyntaxError(current, i, result); } } if (state === FLOAT) { if (RE.DIGIT.test(current)) { number += current; continue; } if (RE.EXP.test(current)) { state = EXP; continue; } if (RE.POINT.test(current) && number[number.length - 1] === ".") { throwSyntaxError(current, i, result); } } if (state === EXP) { if (RE.DIGIT.test(current)) { exponent += current; continue; } if (RE.SIGN.test(current)) { if (exponent === "") { exponent += current; continue; } if (exponent.length === 1 && RE.SIGN.test(exponent)) { throwSyntaxError(current, i, result); } } } if (RE.WHITESPACE.test(current)) { newNumber(); state = SEP; seenComma = false; } else if (RE.COMMA.test(current)) { newNumber(); state = SEP; seenComma = true; } else if (RE.SIGN.test(current)) { newNumber(); state = INT; number = current; } else if (RE.POINT.test(current)) { newNumber(); state = FLOAT; number = current; } else { throwSyntaxError(current, i, result); } } newNumber(); return result; } const units = ["mm", "cm", "in", "pt", "pc", "px"]; const 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) { let theUnit = "px"; if (typeof string === "string" || string instanceof String) { for (let i = 0, n = units.length; i < n; i++) { const u = units[i]; if (string.endsWith(u)) { theUnit = u; string = string.substring(0, string.length - u.length); break; } } } let scale = void 0; if (theUnit === "px" && scope.defaultUnit !== "px") { scale = unitConversion["in"][scope.defaultUnit] / scope.defaultDPI; } else { scale = unitConversion[theUnit][scope.defaultUnit]; if (scale < 0) { scale = unitConversion[theUnit]["in"] * scope.defaultDPI; } } return scale * parseFloat(string); } function getNodeTransform(node) { if (!(node.hasAttribute("transform") || node.nodeName === "use" && (node.hasAttribute("x") || node.hasAttribute("y")))) { return null; } const transform = parseNodeTransform(node); if (transformStack.length > 0) { transform.premultiply(transformStack[transformStack.length - 1]); } currentTransform.copy(transform); transformStack.push(transform); return transform; } function parseNodeTransform(node) { const transform = new Matrix3(); const currentTransform2 = tempTransform0; if (node.nodeName === "use" && (node.hasAttribute("x") || node.hasAttribute("y"))) { const tx = parseFloatWithUnits(node.getAttribute("x")); const ty = parseFloatWithUnits(node.getAttribute("y")); transform.translate(tx, ty); } if (node.hasAttribute("transform")) { const transformsTexts = node.getAttribute("transform").split(")"); for (let tIndex = transformsTexts.length - 1; tIndex >= 0; tIndex--) { const transformText = transformsTexts[tIndex].trim(); if (transformText === "") continue; const openParPos = transformText.indexOf("("); const closeParPos = transformText.length; if (openParPos > 0 && openParPos < closeParPos) { const transformType = transformText.slice(0, openParPos); const array = parseFloats(transformText.slice(openParPos + 1)); currentTransform2.identity(); switch (transformType) { case "translate": if (array.length >= 1) { const tx = array[0]; let ty = 0; if (array.length >= 2) { ty = array[1]; } currentTransform2.translate(tx, ty); } break; case "rotate": if (array.length >= 1) { let angle = 0; let cx = 0; let cy = 0; angle = array[0] * Math.PI / 180; if (array.length >= 3) { cx = array[1]; cy = array[2]; } tempTransform1.makeTranslation(-cx, -cy); tempTransform2.makeRotation(angle); tempTransform3.multiplyMatrices(tempTransform2, tempTransform1); tempTransform1.makeTranslation(cx, cy); currentTransform2.multiplyMatrices(tempTransform1, tempTransform3); } break; case "scale": if (array.length >= 1) { const scaleX = array[0]; let scaleY = scaleX; if (array.length >= 2) { scaleY = array[1]; } currentTransform2.scale(scaleX, scaleY); } break; case "skewX": if (array.length === 1) { currentTransform2.set(1, Math.tan(array[0] * Math.PI / 180), 0, 0, 1, 0, 0, 0, 1); } break; case "skewY": if (array.length === 1) { currentTransform2.set(1, 0, 0, Math.tan(array[0] * Math.PI / 180), 1, 0, 0, 0, 1); } break; case "matrix": if (array.length === 6) { currentTransform2.set(array[0], array[2], array[4], array[1], array[3], array[5], 0, 0, 1); } break; } } transform.premultiply(currentTransform2); } } return transform; } function transformPath(path, m) { function transfVec2(v2) { tempV3.set(v2.x, v2.y, 1).applyMatrix3(m); v2.set(tempV3.x, tempV3.y); } function transfEllipseGeneric(curve) { const a = curve.xRadius; const b = curve.yRadius; const cosTheta = Math.cos(curve.aRotation); const sinTheta = Math.sin(curve.aRotation); const v1 = new Vector3(a * cosTheta, a * sinTheta, 0); const v2 = new Vector3(-b * sinTheta, b * cosTheta, 0); const f1 = v1.applyMatrix3(m); const f2 = v2.applyMatrix3(m); const mF = tempTransform0.set(f1.x, f2.x, 0, f1.y, f2.y, 0, 0, 0, 1); const mFInv = tempTransform1.copy(mF).invert(); const mFInvT = tempTransform2.copy(mFInv).transpose(); const mQ = mFInvT.multiply(mFInv); const mQe = mQ.elements; const ed = eigenDecomposition(mQe[0], mQe[1], mQe[4]); const rt1sqrt = Math.sqrt(ed.rt1); const rt2sqrt = Math.sqrt(ed.rt2); curve.xRadius = 1 / rt1sqrt; curve.yRadius = 1 / rt2sqrt; curve.aRotation = Math.atan2(ed.sn, ed.cs); const isFullEllipse = (curve.aEndAngle - curve.aStartAngle) % (2 * Math.PI) < Number.EPSILON; if (!isFullEllipse) { const mDsqrt = tempTransform1.set(rt1sqrt, 0, 0, 0, rt2sqrt, 0, 0, 0, 1); const mRT = tempTransform2.set(ed.cs, ed.sn, 0, -ed.sn, ed.cs, 0, 0, 0, 1); const mDRF = mDsqrt.multiply(mRT).multiply(mF); const transformAngle = (phi) => { const { x: cosR, y: sinR } = new Vector3(Math.cos(phi), Math.sin(phi), 0).applyMatrix3(mDRF); return Math.atan2(sinR, cosR); }; curve.aStartAngle = transformAngle(curve.aStartAngle); curve.aEndAngle = transformAngle(curve.aEndAngle); if (isTransformFlipped(m)) { curve.aClockwise = !curve.aClockwise; } } } function transfEllipseNoSkew(curve) { const sx = getTransformScaleX(m); const sy = getTransformScaleY(m); curve.xRadius *= sx; curve.yRadius *= sy; const theta = sx > Number.EPSILON ? Math.atan2(m.elements[1], m.elements[0]) : Math.atan2(-m.elements[3], m.elements[4]); curve.aRotation += theta; if (isTransformFlipped(m)) { curve.aStartAngle *= -1; curve.aEndAngle *= -1; curve.aClockwise = !curve.aClockwise; } } const subPaths = path.subPaths; for (let i = 0, n = subPaths.length; i < n; i++) { const subPath = subPaths[i]; const curves = subPath.curves; for (let j = 0; j < curves.length; j++) { const 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) { tempV2.set(curve.aX, curve.aY); transfVec2(tempV2); curve.aX = tempV2.x; curve.aY = tempV2.y; if (isTransformSkewed(m)) { transfEllipseGeneric(curve); } else { transfEllipseNoSkew(curve); } } } } } function isTransformFlipped(m) { const te = m.elements; return te[0] * te[4] - te[1] * te[3] < 0; } function isTransformSkewed(m) { const te = m.elements; const basisDot = te[0] * te[3] + te[1] * te[4]; if (basisDot === 0) return false; const sx = getTransformScaleX(m); const sy = getTransformScaleY(m); return Math.abs(basisDot / (sx * sy)) > Number.EPSILON; } function getTransformScaleX(m) { const te = m.elements; return Math.sqrt(te[0] * te[0] + te[1] * te[1]); } function getTransformScaleY(m) { const te = m.elements; return Math.sqrt(te[3] * te[3] + te[4] * te[4]); } function eigenDecomposition(A, B, C) { let rt1, rt2, cs, sn, t; const sm = A + C; const df = A - C; const rt = Math.sqrt(df * df + 4 * B * B); if (sm > 0) { rt1 = 0.5 * (sm + rt); t = 1 / rt1; rt2 = A * t * C - B * t * B; } else if (sm < 0) { rt2 = 0.5 * (sm - rt); } else { rt1 = 0.5 * rt; rt2 = -0.5 * rt; } if (df > 0) { cs = df + rt; } else { cs = df - rt; } if (Math.abs(cs) > 2 * Math.abs(B)) { t = -2 * B / cs; sn = 1 / Math.sqrt(1 + t * t); cs = t * sn; } else if (Math.abs(B) === 0) { cs = 1; sn = 0; } else { t = -0.5 * cs / B; cs = 1 / Math.sqrt(1 + t * t); sn = t * cs; } if (df > 0) { t = cs; cs = -sn; sn = t; } return { rt1, rt2, cs, sn }; } const paths = []; const stylesheets = {}; const transformStack = []; const tempTransform0 = new Matrix3(); const tempTransform1 = new Matrix3(); const tempTransform2 = new Matrix3(); const tempTransform3 = new Matrix3(); const tempV2 = new Vector2(); const tempV3 = new Vector3(); const currentTransform = new Matrix3(); const xml = new DOMParser().parseFromString(text, "image/svg+xml"); parseNode(xml.documentElement, { fill: "#000", fillOpacity: 1, strokeOpacity: 1, strokeWidth: 1, strokeLineJoin: "miter", strokeLineCap: "butt", strokeMiterLimit: 4 }); const data = { paths, xml: xml.documentElement }; return data; } static createShapes(shapePath) { const BIGNUMBER = 999999999; const IntersectionLocationType = { ORIGIN: 0, DESTINATION: 1, BETWEEN: 2, LEFT: 3, RIGHT: 4, BEHIND: 5, BEYOND: 6 }; const classifyResult = { loc: IntersectionLocationType.ORIGIN, t: 0 }; function findEdgeIntersection(a0, a1, b0, b1) { const x1 = a0.x; const x2 = a1.x; const x3 = b0.x; const x4 = b1.x; const y1 = a0.y; const y2 = a1.y; const y3 = b0.y; const y4 = b1.y; const nom1 = (x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3); const nom2 = (x2 - x1) * (y1 - y3) - (y2 - y1) * (x1 - x3); const denom = (y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1); const t1 = nom1 / denom; const t2 = nom2 / denom; if (denom === 0 && nom1 !== 0 || t1 <= 0 || t1 >= 1 || t2 < 0 || t2 > 1) { return null; } else if (nom1 === 0 && denom === 0) { for (let i = 0; i < 2; i++) { classifyPoint(i === 0 ? b0 : b1, a0, a1); if (classifyResult.loc == IntersectionLocationType.ORIGIN) { const point = i === 0 ? b0 : b1; return { x: point.x, y: point.y, t: classifyResult.t }; } else if (classifyResult.loc == IntersectionLocationType.BETWEEN) { const x = +(x1 + classifyResult.t * (x2 - x1)).toPrecision(10); const y = +(y1 + classifyResult.t * (y2 - y1)).toPrecision(10); return { x, y, t: classifyResult.t }; } } return null; } else { for (let i = 0; i < 2; i++) { classifyPoint(i === 0 ? b0 : b1, a0, a1); if (classifyResult.loc == IntersectionLocationType.ORIGIN) { const point = i === 0 ? b0 : b1; return { x: point.x, y: point.y, t: classifyResult.t }; } } const x = +(x1 + t1 * (x2 - x1)).toPrecision(10); const y = +(y1 + t1 * (y2 - y1)).toPrecision(10); return { x, y, t: t1 }; } } function classifyPoint(p, edgeStart, edgeEnd) { const ax = edgeEnd.x - edgeStart.x; const ay = edgeEnd.y - edgeStart.y; const bx = p.x - edgeStart.x; const by = p.y - edgeStart.y; const sa = ax * by - bx * ay; if (p.x === edgeStart.x && p.y === edgeStart.y) { classifyResult.loc = IntersectionLocationType.ORIGIN; classifyResult.t = 0; return; } if (p.x === edgeEnd.x && p.y === edgeEnd.y) { classifyResult.loc = IntersectionLocationType.DESTINATION; classifyResult.t = 1; return; } if (sa < -Number.EPSILON) { classifyResult.loc = IntersectionLocationType.LEFT; return; } if (sa > Number.EPSILON) { classifyResult.loc = IntersectionLocationType.RIGHT; return; } if (ax * bx < 0 || ay * by < 0) { classifyResult.loc = IntersectionLocationType.BEHIND; return; } if (Math.sqrt(ax * ax + ay * ay) < Math.sqrt(bx * bx + by * by)) { classifyResult.loc = IntersectionLocationType.BEYOND; return; } let t; if (ax !== 0) { t = bx / ax; } else { t = by / ay; } classifyResult.loc = IntersectionLocationType.BETWEEN; classifyResult.t = t; } function getIntersections(path1, path2) { const intersectionsRaw = []; const intersections = []; for (let index = 1; index < path1.length; index++) { const path1EdgeStart = path1[index - 1]; const path1EdgeEnd = path1[index]; for (let index2 = 1; index2 < path2.length; index2++) { const path2EdgeStart = path2[index2 - 1]; const path2EdgeEnd = path2[index2]; const intersection = findEdgeIntersection(path1EdgeStart, path1EdgeEnd, path2EdgeStart, path2EdgeEnd); if (intersection !== null && intersectionsRaw.find( (i) => i.t <= intersection.t + Number.EPSILON && i.t >= intersection.t - Number.EPSILON ) === void 0) { intersectionsRaw.push(intersection); intersections.push(new Vector2(intersection.x, intersection.y)); } } } return intersections; } function getScanlineIntersections(scanline, boundingBox, paths) { const center = new Vector2(); boundingBox.getCenter(center); const allIntersections = []; paths.forEach((path) => { if (path.boundingBox.containsPoint(center)) { const intersections = getIntersections(scanline, path.points); intersections.forEach((p) => { allIntersections.push({ identifier: path.identifier, isCW: path.isCW, point: p }); }); } }); allIntersections.sort((i1, i2) => { return i1.point.x - i2.point.x; }); return allIntersections; } function isHoleTo(simplePath, allPaths, scanlineMinX2, scanlineMaxX2, _fillRule) { if (_fillRule === null || _fillRule === void 0 || _fillRule === "") { _fillRule = "nonzero"; } const centerBoundingBox = new Vector2(); simplePath.boundingBox.getCenter(centerBoundingBox); const scanline = [ new Vector2(scanlineMinX2, centerBoundingBox.y), new Vector2(scanlineMaxX2, centerBoundingBox.y) ]; const scanlineIntersections = getScanlineIntersections(scanline, simplePath.boundingBox, allPaths); scanlineIntersections.sort((i1, i2) => { return i1.point.x - i2.point.x; }); const baseIntersections = []; const otherIntersections = []; scanlineIntersections.forEach((i2) => { if (i2.identifier === simplePath.identifier) { baseIntersections.push(i2); } else { otherIntersections.push(i2); } }); const firstXOfPath = baseIntersections[0].point.x; const stack = []; let i = 0; while (i < otherIntersections.length && otherIntersections[i].point.x < firstXOfPath) { if (stack.length > 0 && stack[stack.length - 1] === otherIntersections[i].identifier) { stack.pop(); } else { stack.push(otherIntersections[i].identifier); } i++; } stack.push(simplePath.identifier); if (_fillRule === "evenodd") { const isHole = stack.length % 2 === 0 ? true : false; const isHoleFor = stack[stack.length - 2]; return { identifier: simplePath.identifier, isHole, for: isHoleFor }; } else if (_fillRule === "nonzero") { let isHole = true; let isHoleFor = null; let lastCWValue = null; for (let i2 = 0; i2 < stack.length; i2++) { const identifier = stack[i2]; if (isHole) { lastCWValue = allPaths[identifier].isCW; isHole = false; isHoleFor = identifier; } else if (lastCWValue !== allPaths[identifier].isCW) { lastCWValue = allPaths[identifier].isCW; isHole = true; } } return { identifier: simplePath.identifier, isHole, for: isHoleFor }; } else { console.warn('fill-rule: "' + _fillRule + '" is currently not implemented.'); } } let scanlineMinX = BIGNUMBER; let scanlineMaxX = -BIGNUMBER; let simplePaths = shapePath.subPaths.map((p) => { const points = p.getPoints(); let maxY = -BIGNUMBER; let minY = BIGNUMBER; let maxX = -BIGNUMBER; let minX = BIGNUMBER; for (let i = 0; i < points.length; i++) { const p2 = points[i]; if (p2.y > maxY) { maxY = p2.y; } if (p2.y < minY) { minY = p2.y; } if (p2.x > maxX) { maxX = p2.x; } if (p2.x < minX) { minX = p2.x; } } if (scanlineMaxX <= maxX) { scanlineMaxX = maxX + 1; } if (scanlineMinX >= minX) { scanlineMinX = minX - 1; } return { curves: p.curves, points, isCW: ShapeUtils.isClockWise(points), identifier: -1, boundingBox: new Box2(new Vector2(minX, minY), new Vector2(maxX, maxY)) }; });