Spaces:
Sleeping
Sleeping
| // Transform SVG PathData | |
| // http://www.w3.org/TR/SVG/paths.html#PathDataBNF | |
| import { a2c, annotateArcCommand, arcAt, assertNumbers, bezierAt, bezierRoot, | |
| intersectionUnitCircleLine } from "./mathUtils"; | |
| import { SVGPathData } from "./SVGPathData"; | |
| import { SVGCommand, TransformFunction } from "./types"; | |
| export namespace SVGPathDataTransformer { | |
| // Predefined transforming functions | |
| // Rounds commands values | |
| export function ROUND(roundVal = 1e13) { | |
| assertNumbers(roundVal); | |
| function rf(val: number) { return Math.round(val * roundVal) / roundVal; } | |
| return function round(command: any) { | |
| if ("undefined" !== typeof command.x1) { | |
| command.x1 = rf(command.x1); | |
| } | |
| if ("undefined" !== typeof command.y1) { | |
| command.y1 = rf(command.y1); | |
| } | |
| if ("undefined" !== typeof command.x2) { | |
| command.x2 = rf(command.x2); | |
| } | |
| if ("undefined" !== typeof command.y2) { | |
| command.y2 = rf(command.y2); | |
| } | |
| if ("undefined" !== typeof command.x) { | |
| command.x = rf(command.x); | |
| } | |
| if ("undefined" !== typeof command.y) { | |
| command.y = rf(command.y); | |
| } | |
| if ("undefined" !== typeof command.rX) { | |
| command.rX = rf(command.rX); | |
| } | |
| if ("undefined" !== typeof command.rY) { | |
| command.rY = rf(command.rY); | |
| } | |
| return command; | |
| }; | |
| } | |
| // Relative to absolute commands | |
| export function TO_ABS() { | |
| return INFO((command, prevX, prevY) => { | |
| if (command.relative) { | |
| // x1/y1 values | |
| if ("undefined" !== typeof command.x1) { | |
| command.x1 += prevX; | |
| } | |
| if ("undefined" !== typeof command.y1) { | |
| command.y1 += prevY; | |
| } | |
| // x2/y2 values | |
| if ("undefined" !== typeof command.x2) { | |
| command.x2 += prevX; | |
| } | |
| if ("undefined" !== typeof command.y2) { | |
| command.y2 += prevY; | |
| } | |
| // Finally x/y values | |
| if ("undefined" !== typeof command.x) { | |
| command.x += prevX; | |
| } | |
| if ("undefined" !== typeof command.y) { | |
| command.y += prevY; | |
| } | |
| command.relative = false; | |
| } | |
| return command; | |
| }); | |
| } | |
| // Absolute to relative commands | |
| export function TO_REL() { | |
| return INFO((command, prevX, prevY) => { | |
| if (!command.relative) { | |
| // x1/y1 values | |
| if ("undefined" !== typeof command.x1) { | |
| command.x1 -= prevX; | |
| } | |
| if ("undefined" !== typeof command.y1) { | |
| command.y1 -= prevY; | |
| } | |
| // x2/y2 values | |
| if ("undefined" !== typeof command.x2) { | |
| command.x2 -= prevX; | |
| } | |
| if ("undefined" !== typeof command.y2) { | |
| command.y2 -= prevY; | |
| } | |
| // Finally x/y values | |
| if ("undefined" !== typeof command.x) { | |
| command.x -= prevX; | |
| } | |
| if ("undefined" !== typeof command.y) { | |
| command.y -= prevY; | |
| } | |
| command.relative = true; | |
| } | |
| return command; | |
| }); | |
| } | |
| // Convert H, V, Z and A with rX = 0 to L | |
| export function NORMALIZE_HVZ(normalizeZ = true, normalizeH = true, normalizeV = true) { | |
| return INFO((command, prevX, prevY, pathStartX, pathStartY) => { | |
| if (isNaN(pathStartX) && !(command.type & SVGPathData.MOVE_TO)) { | |
| throw new Error("path must start with moveto"); | |
| } | |
| if (normalizeH && command.type & SVGPathData.HORIZ_LINE_TO) { | |
| command.type = SVGPathData.LINE_TO; | |
| command.y = command.relative ? 0 : prevY; | |
| } | |
| if (normalizeV && command.type & SVGPathData.VERT_LINE_TO) { | |
| command.type = SVGPathData.LINE_TO; | |
| command.x = command.relative ? 0 : prevX; | |
| } | |
| if (normalizeZ && command.type & SVGPathData.CLOSE_PATH) { | |
| command.type = SVGPathData.LINE_TO; | |
| command.x = command.relative ? pathStartX - prevX : pathStartX; | |
| command.y = command.relative ? pathStartY - prevY : pathStartY; | |
| } | |
| if (command.type & SVGPathData.ARC && (0 === command.rX || 0 === command.rY)) { | |
| command.type = SVGPathData.LINE_TO; | |
| delete command.rX; | |
| delete command.rY; | |
| delete command.xRot; | |
| delete command.lArcFlag; | |
| delete command.sweepFlag; | |
| } | |
| return command; | |
| }); | |
| } | |
| /* | |
| * Transforms smooth curves and quads to normal curves and quads (SsTt to CcQq) | |
| */ | |
| export function NORMALIZE_ST() { | |
| let prevCurveC2X = NaN; | |
| let prevCurveC2Y = NaN; | |
| let prevQuadCX = NaN; | |
| let prevQuadCY = NaN; | |
| return INFO((command, prevX, prevY) => { | |
| if (command.type & SVGPathData.SMOOTH_CURVE_TO) { | |
| command.type = SVGPathData.CURVE_TO; | |
| prevCurveC2X = isNaN(prevCurveC2X) ? prevX : prevCurveC2X; | |
| prevCurveC2Y = isNaN(prevCurveC2Y) ? prevY : prevCurveC2Y; | |
| command.x1 = command.relative ? prevX - prevCurveC2X : 2 * prevX - prevCurveC2X; | |
| command.y1 = command.relative ? prevY - prevCurveC2Y : 2 * prevY - prevCurveC2Y; | |
| } | |
| if (command.type & SVGPathData.CURVE_TO) { | |
| prevCurveC2X = command.relative ? prevX + command.x2 : command.x2; | |
| prevCurveC2Y = command.relative ? prevY + command.y2 : command.y2; | |
| } else { | |
| prevCurveC2X = NaN; | |
| prevCurveC2Y = NaN; | |
| } | |
| if (command.type & SVGPathData.SMOOTH_QUAD_TO) { | |
| command.type = SVGPathData.QUAD_TO; | |
| prevQuadCX = isNaN(prevQuadCX) ? prevX : prevQuadCX; | |
| prevQuadCY = isNaN(prevQuadCY) ? prevY : prevQuadCY; | |
| command.x1 = command.relative ? prevX - prevQuadCX : 2 * prevX - prevQuadCX; | |
| command.y1 = command.relative ? prevY - prevQuadCY : 2 * prevY - prevQuadCY; | |
| } | |
| if (command.type & SVGPathData.QUAD_TO) { | |
| prevQuadCX = command.relative ? prevX + command.x1 : command.x1; | |
| prevQuadCY = command.relative ? prevY + command.y1 : command.y1; | |
| } else { | |
| prevQuadCX = NaN; | |
| prevQuadCY = NaN; | |
| } | |
| return command; | |
| }); | |
| } | |
| /* | |
| * A quadratic bézier curve can be represented by a cubic bézier curve which has | |
| * the same end points as the quadratic and both control points in place of the | |
| * quadratic"s one. | |
| * | |
| * This transformer replaces QqTt commands with Cc commands respectively. | |
| * This is useful for reading path data into a system which only has a | |
| * representation for cubic curves. | |
| */ | |
| export function QT_TO_C() { | |
| let prevQuadX1 = NaN; | |
| let prevQuadY1 = NaN; | |
| return INFO((command, prevX, prevY) => { | |
| if (command.type & SVGPathData.SMOOTH_QUAD_TO) { | |
| command.type = SVGPathData.QUAD_TO; | |
| prevQuadX1 = isNaN(prevQuadX1) ? prevX : prevQuadX1; | |
| prevQuadY1 = isNaN(prevQuadY1) ? prevY : prevQuadY1; | |
| command.x1 = command.relative ? prevX - prevQuadX1 : 2 * prevX - prevQuadX1; | |
| command.y1 = command.relative ? prevY - prevQuadY1 : 2 * prevY - prevQuadY1; | |
| } | |
| if (command.type & SVGPathData.QUAD_TO) { | |
| prevQuadX1 = command.relative ? prevX + command.x1 : command.x1; | |
| prevQuadY1 = command.relative ? prevY + command.y1 : command.y1; | |
| const x1 = command.x1; | |
| const y1 = command.y1; | |
| command.type = SVGPathData.CURVE_TO; | |
| command.x1 = ((command.relative ? 0 : prevX) + x1 * 2) / 3; | |
| command.y1 = ((command.relative ? 0 : prevY) + y1 * 2) / 3; | |
| command.x2 = (command.x + x1 * 2) / 3; | |
| command.y2 = (command.y + y1 * 2) / 3; | |
| } else { | |
| prevQuadX1 = NaN; | |
| prevQuadY1 = NaN; | |
| } | |
| return command; | |
| }); | |
| } | |
| export function INFO( | |
| f: (command: any, prevXAbs: number, prevYAbs: number, | |
| pathStartXAbs: number, pathStartYAbs: number) => any | any[]) { | |
| let prevXAbs = 0; | |
| let prevYAbs = 0; | |
| let pathStartXAbs = NaN; | |
| let pathStartYAbs = NaN; | |
| return function transform(command: any) { | |
| if (isNaN(pathStartXAbs) && !(command.type & SVGPathData.MOVE_TO)) { | |
| throw new Error("path must start with moveto"); | |
| } | |
| const result = f(command, prevXAbs, prevYAbs, pathStartXAbs, pathStartYAbs); | |
| if (command.type & SVGPathData.CLOSE_PATH) { | |
| prevXAbs = pathStartXAbs; | |
| prevYAbs = pathStartYAbs; | |
| } | |
| if ("undefined" !== typeof command.x) { | |
| prevXAbs = (command.relative ? prevXAbs + command.x : command.x); | |
| } | |
| if ("undefined" !== typeof command.y) { | |
| prevYAbs = (command.relative ? prevYAbs + command.y : command.y); | |
| } | |
| if (command.type & SVGPathData.MOVE_TO) { | |
| pathStartXAbs = prevXAbs; | |
| pathStartYAbs = prevYAbs; | |
| } | |
| return result; | |
| }; | |
| } | |
| /* | |
| * remove 0-length segments | |
| */ | |
| export function SANITIZE(EPS = 0) { | |
| assertNumbers(EPS); | |
| let prevCurveC2X = NaN; | |
| let prevCurveC2Y = NaN; | |
| let prevQuadCX = NaN; | |
| let prevQuadCY = NaN; | |
| return INFO((command, prevX, prevY, pathStartX, pathStartY) => { | |
| const abs = Math.abs; | |
| let skip = false; | |
| let x1Rel = 0; | |
| let y1Rel = 0; | |
| if (command.type & SVGPathData.SMOOTH_CURVE_TO) { | |
| x1Rel = isNaN(prevCurveC2X) ? 0 : prevX - prevCurveC2X; | |
| y1Rel = isNaN(prevCurveC2Y) ? 0 : prevY - prevCurveC2Y; | |
| } | |
| if (command.type & (SVGPathData.CURVE_TO | SVGPathData.SMOOTH_CURVE_TO)) { | |
| prevCurveC2X = command.relative ? prevX + command.x2 : command.x2; | |
| prevCurveC2Y = command.relative ? prevY + command.y2 : command.y2; | |
| } else { | |
| prevCurveC2X = NaN; | |
| prevCurveC2Y = NaN; | |
| } | |
| if (command.type & SVGPathData.SMOOTH_QUAD_TO) { | |
| prevQuadCX = isNaN(prevQuadCX) ? prevX : 2 * prevX - prevQuadCX; | |
| prevQuadCY = isNaN(prevQuadCY) ? prevY : 2 * prevY - prevQuadCY; | |
| } else if (command.type & SVGPathData.QUAD_TO) { | |
| prevQuadCX = command.relative ? prevX + command.x1 : command.x1; | |
| prevQuadCY = command.relative ? prevY + command.y1 : command.y2; | |
| } else { | |
| prevQuadCX = NaN; | |
| prevQuadCY = NaN; | |
| } | |
| if (command.type & SVGPathData.LINE_COMMANDS || | |
| command.type & SVGPathData.ARC && (0 === command.rX || 0 === command.rY || !command.lArcFlag) || | |
| command.type & SVGPathData.CURVE_TO || command.type & SVGPathData.SMOOTH_CURVE_TO || | |
| command.type & SVGPathData.QUAD_TO || command.type & SVGPathData.SMOOTH_QUAD_TO) { | |
| const xRel = "undefined" === typeof command.x ? 0 : | |
| (command.relative ? command.x : command.x - prevX); | |
| const yRel = "undefined" === typeof command.y ? 0 : | |
| (command.relative ? command.y : command.y - prevY); | |
| x1Rel = !isNaN(prevQuadCX) ? prevQuadCX - prevX : | |
| "undefined" === typeof command.x1 ? x1Rel : | |
| command.relative ? command.x : | |
| command.x1 - prevX; | |
| y1Rel = !isNaN(prevQuadCY) ? prevQuadCY - prevY : | |
| "undefined" === typeof command.y1 ? y1Rel : | |
| command.relative ? command.y : | |
| command.y1 - prevY; | |
| const x2Rel = "undefined" === typeof command.x2 ? 0 : | |
| (command.relative ? command.x : command.x2 - prevX); | |
| const y2Rel = "undefined" === typeof command.y2 ? 0 : | |
| (command.relative ? command.y : command.y2 - prevY); | |
| if (abs(xRel) <= EPS && abs(yRel) <= EPS && | |
| abs(x1Rel) <= EPS && abs(y1Rel) <= EPS && | |
| abs(x2Rel) <= EPS && abs(y2Rel) <= EPS) { | |
| skip = true; | |
| } | |
| } | |
| if (command.type & SVGPathData.CLOSE_PATH) { | |
| if (abs(prevX - pathStartX) <= EPS && abs(prevY - pathStartY) <= EPS) { | |
| skip = true; | |
| } | |
| } | |
| return skip ? [] : command; | |
| }); | |
| } | |
| // SVG Transforms : http://www.w3.org/TR/SVGTiny12/coords.html#TransformList | |
| // Matrix : http://apike.ca/prog_svg_transform.html | |
| // a c e | |
| // b d f | |
| export function MATRIX(a: number, b: number, c: number, d: number, e: number, f: number) { | |
| assertNumbers(a, b, c, d, e, f); | |
| return INFO((command, prevX, prevY, pathStartX) => { | |
| const origX1 = command.x1; | |
| const origX2 = command.x2; | |
| // if isNaN(pathStartX), then this is the first command, which is ALWAYS an | |
| // absolute MOVE_TO, regardless what the relative flag says | |
| const comRel = command.relative && !isNaN(pathStartX); | |
| const x = "undefined" !== typeof command.x ? command.x : (comRel ? 0 : prevX); | |
| const y = "undefined" !== typeof command.y ? command.y : (comRel ? 0 : prevY); | |
| if (command.type & SVGPathData.HORIZ_LINE_TO && 0 !== b) { | |
| command.type = SVGPathData.LINE_TO; | |
| command.y = command.relative ? 0 : prevY; | |
| } | |
| if (command.type & SVGPathData.VERT_LINE_TO && 0 !== c) { | |
| command.type = SVGPathData.LINE_TO; | |
| command.x = command.relative ? 0 : prevX; | |
| } | |
| if ("undefined" !== typeof command.x) { | |
| command.x = (command.x * a) + (y * c) + (comRel ? 0 : e); | |
| } | |
| if ("undefined" !== typeof command.y) { | |
| command.y = (x * b) + command.y * d + (comRel ? 0 : f); | |
| } | |
| if ("undefined" !== typeof command.x1) { | |
| command.x1 = command.x1 * a + command.y1 * c + (comRel ? 0 : e); | |
| } | |
| if ("undefined" !== typeof command.y1) { | |
| command.y1 = origX1 * b + command.y1 * d + (comRel ? 0 : f); | |
| } | |
| if ("undefined" !== typeof command.x2) { | |
| command.x2 = command.x2 * a + command.y2 * c + (comRel ? 0 : e); | |
| } | |
| if ("undefined" !== typeof command.y2) { | |
| command.y2 = origX2 * b + command.y2 * d + (comRel ? 0 : f); | |
| } | |
| function sqr(x: number) { return x * x; } | |
| const det = a * d - b * c; | |
| if ("undefined" !== typeof command.xRot) { | |
| // Skip if this is a pure translation | |
| if (1 !== a || 0 !== b || 0 !== c || 1 !== d) { | |
| // Special case for singular matrix | |
| if (0 === det) { | |
| // In the singular case, the arc is compressed to a line. The actual geometric image of the original | |
| // curve under this transform possibly extends beyond the starting and/or ending points of the segment, but | |
| // for simplicity we ignore this detail and just replace this command with a single line segment. | |
| delete command.rX; | |
| delete command.rY; | |
| delete command.xRot; | |
| delete command.lArcFlag; | |
| delete command.sweepFlag; | |
| command.type = SVGPathData.LINE_TO; | |
| } else { | |
| // Convert to radians | |
| const xRot = command.xRot * Math.PI / 180; | |
| // Convert rotated ellipse to general conic form | |
| // x0^2/rX^2 + y0^2/rY^2 - 1 = 0 | |
| // x0 = x*cos(xRot) + y*sin(xRot) | |
| // y0 = -x*sin(xRot) + y*cos(xRot) | |
| // --> A*x^2 + B*x*y + C*y^2 - 1 = 0, where | |
| const sinRot = Math.sin(xRot); | |
| const cosRot = Math.cos(xRot); | |
| const xCurve = 1 / sqr(command.rX); | |
| const yCurve = 1 / sqr(command.rY); | |
| const A = sqr(cosRot) * xCurve + sqr(sinRot) * yCurve; | |
| const B = 2 * sinRot * cosRot * (xCurve - yCurve); | |
| const C = sqr(sinRot) * xCurve + sqr(cosRot) * yCurve; | |
| // Apply matrix to A*x^2 + B*x*y + C*y^2 - 1 = 0 | |
| // x1 = a*x + c*y | |
| // y1 = b*x + d*y | |
| // (we can ignore e and f, since pure translations don"t affect the shape of the ellipse) | |
| // --> A1*x1^2 + B1*x1*y1 + C1*y1^2 - det^2 = 0, where | |
| const A1 = A * d * d - B * b * d + C * b * b; | |
| const B1 = B * (a * d + b * c) - 2 * (A * c * d + C * a * b); | |
| const C1 = A * c * c - B * a * c + C * a * a; | |
| // Unapply newXRot to get back to axis-aligned ellipse equation | |
| // x1 = x2*cos(newXRot) - y2*sin(newXRot) | |
| // y1 = x2*sin(newXRot) + y2*cos(newXRot) | |
| // A1*x1^2 + B1*x1*y1 + C1*y1^2 - det^2 = | |
| // x2^2*(A1*cos(newXRot)^2 + B1*sin(newXRot)*cos(newXRot) + C1*sin(newXRot)^2) | |
| // + x2*y2*(2*(C1 - A1)*sin(newXRot)*cos(newXRot) + B1*(cos(newXRot)^2 - sin(newXRot)^2)) | |
| // + y2^2*(A1*sin(newXRot)^2 - B1*sin(newXRot)*cos(newXRot) + C1*cos(newXRot)^2) | |
| // (which must have the same zeroes as) | |
| // x2^2/newRX^2 + y2^2/newRY^2 - 1 | |
| // (so we have) | |
| // 2*(C1 - A1)*sin(newXRot)*cos(newXRot) + B1*(cos(newXRot)^2 - sin(newXRot)^2) = 0 | |
| // (A1 - C1)*sin(2*newXRot) = B1*cos(2*newXRot) | |
| // 2*newXRot = atan2(B1, A1 - C1) | |
| const newXRot = ((Math.atan2(B1, A1 - C1) + Math.PI) % Math.PI) / 2; | |
| // For any integer n, (atan2(B1, A1 - C1) + n*pi)/2 is a solution to the above; incrementing n just swaps | |
| // the x and y radii computed below (since that"s what rotating an ellipse by pi/2 does). Choosing the | |
| // rotation between 0 and pi/2 eliminates the ambiguity and leads to more predictable output. | |
| // Finally, we get newRX and newRY from the same-zeroes relationship that gave us newXRot | |
| const newSinRot = Math.sin(newXRot); | |
| const newCosRot = Math.cos(newXRot); | |
| command.rX = Math.abs(det) / | |
| Math.sqrt(A1 * sqr(newCosRot) + B1 * newSinRot * newCosRot + C1 * sqr(newSinRot)); | |
| command.rY = Math.abs(det) / | |
| Math.sqrt(A1 * sqr(newSinRot) - B1 * newSinRot * newCosRot + C1 * sqr(newCosRot)); | |
| command.xRot = newXRot * 180 / Math.PI; | |
| } | |
| } | |
| } | |
| // sweepFlag needs to be inverted when mirroring shapes | |
| // see http://www.itk.ilstu.edu/faculty/javila/SVG/SVG_drawing1/elliptical_curve.htm | |
| // m 65,10 a 50,25 0 1 0 50,25 | |
| // M 65,60 A 50,25 0 1 1 115,35 | |
| if ("undefined" !== typeof command.sweepFlag && 0 > det) { | |
| command.sweepFlag = +!command.sweepFlag; | |
| } | |
| return command; | |
| }); | |
| } | |
| export function ROTATE(a: number, x = 0, y = 0) { | |
| assertNumbers(a, x, y); | |
| const sin = Math.sin(a); | |
| const cos = Math.cos(a); | |
| return MATRIX(cos, sin, -sin, cos, x - x * cos + y * sin, y - x * sin - y * cos); | |
| } | |
| export function TRANSLATE(dX: number, dY = 0) { | |
| assertNumbers(dX, dY); | |
| return MATRIX(1, 0, 0, 1, dX, dY); | |
| } | |
| export function SCALE(dX: number, dY = dX) { | |
| assertNumbers(dX, dY); | |
| return MATRIX(dX, 0, 0, dY, 0, 0); | |
| } | |
| export function SKEW_X(a: number) { | |
| assertNumbers(a); | |
| return MATRIX(1, 0, Math.atan(a), 1, 0, 0); | |
| } | |
| export function SKEW_Y(a: number) { | |
| assertNumbers(a); | |
| return MATRIX(1, Math.atan(a), 0, 1, 0, 0); | |
| } | |
| export function X_AXIS_SYMMETRY(xOffset = 0) { | |
| assertNumbers(xOffset); | |
| return MATRIX(-1, 0, 0, 1, xOffset, 0); | |
| } | |
| export function Y_AXIS_SYMMETRY(yOffset = 0) { | |
| assertNumbers(yOffset); | |
| return MATRIX(1, 0, 0, -1, 0, yOffset); | |
| } | |
| // Convert arc commands to curve commands | |
| export function A_TO_C() { | |
| return INFO((command, prevX, prevY) => { | |
| if (SVGPathData.ARC === command.type) { | |
| return a2c(command, command.relative ? 0 : prevX, command.relative ? 0 : prevY); | |
| } | |
| return command; | |
| }); | |
| } | |
| // @see annotateArcCommand | |
| export function ANNOTATE_ARCS() { | |
| return INFO((c, x1, y1) => { | |
| if (c.relative) { | |
| x1 = 0; | |
| y1 = 0; | |
| } | |
| if (SVGPathData.ARC === c.type) { | |
| annotateArcCommand(c, x1, y1); | |
| } | |
| return c; | |
| }); | |
| } | |
| export function CLONE() { | |
| return (c: SVGCommand) => { | |
| const result = {} as SVGCommand; | |
| // tslint:disable-next-line | |
| for (const key in c) { | |
| result[key as keyof SVGCommand] = c[key as keyof SVGCommand]; | |
| } | |
| return result; | |
| }; | |
| } | |
| // @see annotateArcCommand | |
| export function CALCULATE_BOUNDS() { | |
| const clone = CLONE(); | |
| const toAbs = TO_ABS(); | |
| const qtToC = QT_TO_C(); | |
| const normST = NORMALIZE_ST(); | |
| const f: TransformFunction & {minX: number, maxX: number, minY: number, maxY: number} = | |
| INFO((command, prevXAbs, prevYAbs) => { | |
| const c = normST(qtToC(toAbs(clone(command)))); | |
| function fixX(absX: number) { | |
| if (absX > f.maxX) { f.maxX = absX; } | |
| if (absX < f.minX) { f.minX = absX; } | |
| } | |
| function fixY(absY: number) { | |
| if (absY > f.maxY) { f.maxY = absY; } | |
| if (absY < f.minY) { f.minY = absY; } | |
| } | |
| if (c.type & SVGPathData.DRAWING_COMMANDS) { | |
| fixX(prevXAbs); | |
| fixY(prevYAbs); | |
| } | |
| if (c.type & SVGPathData.HORIZ_LINE_TO) { | |
| fixX(c.x); | |
| } | |
| if (c.type & SVGPathData.VERT_LINE_TO) { | |
| fixY(c.y); | |
| } | |
| if (c.type & SVGPathData.LINE_TO) { | |
| fixX(c.x); | |
| fixY(c.y); | |
| } | |
| if (c.type & SVGPathData.CURVE_TO) { | |
| // add start and end points | |
| fixX(c.x); | |
| fixY(c.y); | |
| const xDerivRoots = bezierRoot(prevXAbs, c.x1, c.x2, c.x); | |
| for (const derivRoot of xDerivRoots) { | |
| if (0 < derivRoot && 1 > derivRoot) { | |
| fixX(bezierAt(prevXAbs, c.x1, c.x2, c.x, derivRoot)); | |
| } | |
| } | |
| const yDerivRoots = bezierRoot(prevYAbs, c.y1, c.y2, c.y); | |
| for (const derivRoot of yDerivRoots) { | |
| if (0 < derivRoot && 1 > derivRoot) { | |
| fixY(bezierAt(prevYAbs, c.y1, c.y2, c.y, derivRoot)); | |
| } | |
| } | |
| } | |
| if (c.type & SVGPathData.ARC) { | |
| // add start and end points | |
| fixX(c.x); | |
| fixY(c.y); | |
| annotateArcCommand(c, prevXAbs, prevYAbs); | |
| // p = cos(phi) * xv + sin(phi) * yv | |
| // dp = -sin(phi) * xv + cos(phi) * yv = 0 | |
| const xRotRad = c.xRot / 180 * Math.PI; | |
| // points on ellipse for phi = 0° and phi = 90° | |
| const x0 = Math.cos(xRotRad) * c.rX; | |
| const y0 = Math.sin(xRotRad) * c.rX; | |
| const x90 = -Math.sin(xRotRad) * c.rY; | |
| const y90 = Math.cos(xRotRad) * c.rY; | |
| // annotateArcCommand returns phi1 and phi2 such that -180° < phi1 < 180° and phi2 is smaller or greater | |
| // depending on the sweep flag. Calculate phiMin, phiMax such that -180° < phiMin < 180° and phiMin < phiMax | |
| const [phiMin, phiMax] = c.phi1 < c.phi2 ? | |
| [c.phi1, c.phi2] : | |
| (-180 > c.phi2 ? [c.phi2 + 360, c.phi1 + 360] : [c.phi2, c.phi1]); | |
| const normalizeXiEta = ([xi, eta]: [number, number]) => { | |
| const phiRad = Math.atan2(eta, xi); | |
| const phi = phiRad * 180 / Math.PI; | |
| return phi < phiMin ? phi + 360 : phi; | |
| }; | |
| // xi = cos(phi), eta = sin(phi) | |
| const xDerivRoots = intersectionUnitCircleLine(x90, -x0, 0).map(normalizeXiEta); | |
| for (const derivRoot of xDerivRoots) { | |
| if (derivRoot > phiMin && derivRoot < phiMax) { | |
| fixX(arcAt(c.cX, x0, x90, derivRoot)); | |
| } | |
| } | |
| const yDerivRoots = intersectionUnitCircleLine(y90, -y0, 0).map(normalizeXiEta); | |
| for (const derivRoot of yDerivRoots) { | |
| if (derivRoot > phiMin && derivRoot < phiMax) { | |
| fixY(arcAt(c.cY, y0, y90, derivRoot)); | |
| } | |
| } | |
| } | |
| return command; | |
| }) as any; | |
| f.minX = Infinity; | |
| f.maxX = -Infinity; | |
| f.minY = Infinity; | |
| f.maxY = -Infinity; | |
| return f; | |
| } | |
| } | |