diff --git a/.npmignore b/.npmignore index 16c7a6e..151fdd9 100644 --- a/.npmignore +++ b/.npmignore @@ -1,6 +1,5 @@ * !LICENSE -!src/ !dist/ !package.json !CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md index b86e8bf..ec946cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,39 +1,45 @@ # CHANGELOG -## [1.2.3] - 2022-12-23 +## 1.2.4 - 2023-02-09 + +- Optimized proccesing of segments based on [Simple Icons] data. + +[Simple Icons]: https://github.com/simple-icons/simple-icons + +## 1.2.3 - 2022-12-23 - Fixed CLI not being executed in some versions of Node.js < v16. -## [1.2.2] - 2022-05-26 +## 1.2.2 - 2022-05-26 - Fixed edge case computing cubic Bézier curves bounding boxes. -## [1.2.1] - 2022-05-12 +## 1.2.1 - 2022-05-12 - Fixed error computing cubic Bézier curves bounding boxes. -## [1.2.0] - 2022-05-07 +## 1.2.0 - 2022-05-07 - Use default export for better interoperability. -## [1.1.0] - 2022-05-05 +## 1.1.0 - 2022-05-05 - Add support for Typescript. -## [1.0.2] - 2022-01-11 +## 1.0.2 - 2022-01-11 - Add basic options `--version` and `--help` to CLI. -## [1.0.1] - 2021-06-21 +## 1.0.1 - 2021-06-21 - Fixed error computing limits for cubic Bèzier curves of length 0. -## [1.0.0] - 2021-06-03 +## 1.0.0 - 2021-06-03 - Testing with 100% coverage. - Make `svgPathBbox` function the default export. -## [0.2.0] - 2020-12-22 +## 0.2.0 - 2020-12-22 - Removed almost all public API functions (only keep `svgPathBbox` function). - Removed `polf` dependency. @@ -41,22 +47,22 @@ - Optimized cubic Bézier curves minimum and maximum values computation. - Optimized lineal segments minimum and maximum values computation. -## [0.1.5] - 2020-11-26 +## 0.1.5 - 2020-11-26 - Documentation improved. - Switch CI to Github Actions. -## [0.1.4] - 2020-11-23 +## 0.1.4 - 2020-11-23 - Document and export `quadraticBezierCurveBbox` function. - Remove development file from NPM package. - Update acknowledgments. -## [0.1.1] - 2020-11-19 +## 0.1.1 - 2020-11-19 - Fix error computing bounding boxes for Q, T and some C commands. -## [0.0.47] - 2020-07-23 +## 0.0.47 - 2020-07-23 - Separate point on line functions in another package. - Replaced svg-path-parser dependency with svgpath to optimize parsing time. @@ -65,25 +71,25 @@ - Fix errors in utility functions. - Add tests for utilities and command line client. -## [0.0.28] - 2020-05-22 +## 0.0.28 - 2020-05-22 - Update LICENSE. - Fix error converting quaratic to Bézier coordinates. - Add tests for some bounding boxes functions. -## [0.0.26] - 2020-05-21 +## 0.0.26 - 2020-05-21 - Removed `quadraticBezierCurveBbox` function. - Optimized quadratic Bézier curve bounding box computation. - Optimized cubic Bézier curve bounding box algorithm. - Fixed error on V and H commands computing SVG path bbox. -## [0.0.20] - 2020-05-17 +## 0.0.20 - 2020-05-17 - Add function to obtain an array of numbers from SVG path. - Multiple paths as arguments for command line script. -## [0.0.13] - 2020-05-17 +## 0.0.13 - 2020-05-17 - Add command line interface. - Add linting. diff --git a/dist/index.js b/dist/index.js index eede4eb..349945c 100644 --- a/dist/index.js +++ b/dist/index.js @@ -1,5 +1,6 @@ "use strict"; exports.__esModule = true; +// WARNING: This file is autogenerated, edit lib/index.template.ts var svgPath = require("svgpath"); // Precision for consider cubic polynom as quadratic one var CBEZIER_MINMAX_EPSILON = 0.00000001; @@ -75,8 +76,7 @@ function svgPathBbox(d) { .unshort() .iterate(function (seg, _, x, y) { switch (seg[0]) { - case "M": - case "L": { + case "M": { if (min[0] > seg[1]) { min[0] = seg[1]; } @@ -91,6 +91,15 @@ function svgPathBbox(d) { } break; } + case "H": { + if (min[0] > seg[1]) { + min[0] = seg[1]; + } + if (max[0] < seg[1]) { + max[0] = seg[1]; + } + break; + } case "V": { if (min[1] > seg[1]) { min[1] = seg[1]; @@ -100,13 +109,19 @@ function svgPathBbox(d) { } break; } - case "H": { + case "L": { if (min[0] > seg[1]) { min[0] = seg[1]; } + if (min[1] > seg[2]) { + min[1] = seg[2]; + } if (max[0] < seg[1]) { max[0] = seg[1]; } + if (max[1] < seg[2]) { + max[1] = seg[2]; + } break; } case "C": { diff --git a/lib/cases.ts b/lib/cases.ts new file mode 100644 index 0000000..2d0495c --- /dev/null +++ b/lib/cases.ts @@ -0,0 +1,74 @@ +const ML = `{ + if (min[0] > seg[1]) { + min[0] = seg[1]; + } + if (min[1] > seg[2]) { + min[1] = seg[2]; + } + if (max[0] < seg[1]) { + max[0] = seg[1]; + } + if (max[1] < seg[2]) { + max[1] = seg[2]; + } + break; + }`; + +export default { + M: ML, + L: ML, + H: `{ + if (min[0] > seg[1]) { + min[0] = seg[1]; + } + if (max[0] < seg[1]) { + max[0] = seg[1]; + } + break; + }`, + V: `{ + if (min[1] > seg[1]) { + min[1] = seg[1]; + } + if (max[1] < seg[1]) { + max[1] = seg[1]; + } + break; + }`, + C: `{ + const cxMinMax = minmaxC([x, seg[1], seg[3], seg[5]]); + if (min[0] > cxMinMax[0]) { + min[0] = cxMinMax[0]; + } + if (max[0] < cxMinMax[1]) { + max[0] = cxMinMax[1]; + } + + const cyMinMax = minmaxC([y, seg[2], seg[4], seg[6]]); + if (min[1] > cyMinMax[0]) { + min[1] = cyMinMax[0]; + } + if (max[1] < cyMinMax[1]) { + max[1] = cyMinMax[1]; + } + break; + }`, + Q: `{ + const qxMinMax = minmaxQ([x, seg[1], seg[3]]); + if (min[0] > qxMinMax[0]) { + min[0] = qxMinMax[0]; + } + if (max[0] < qxMinMax[1]) { + max[0] = qxMinMax[1]; + } + + const qyMinMax = minmaxQ([y, seg[2], seg[4]]); + if (min[1] > qyMinMax[0]) { + min[1] = qyMinMax[0]; + } + if (max[1] < qyMinMax[1]) { + max[1] = qyMinMax[1]; + } + break; + }`, +} diff --git a/lib/index.template.ts b/lib/index.template.ts new file mode 100644 index 0000000..0815b77 --- /dev/null +++ b/lib/index.template.ts @@ -0,0 +1,103 @@ +"use strict"; + +import * as svgPath from "svgpath"; + +type minMax = [min: number, max: number]; +export type BBox = [minX: number, minY: number, maxX: number, maxY: number]; + +// Precision for consider cubic polynom as quadratic one +const CBEZIER_MINMAX_EPSILON = 0.00000001; + +// https://github.com/kpym/SVGPathy/blob/acd1a50c626b36d81969f6e98e8602e128ba4302/lib/box.js#L89 +function minmaxQ(A: [number, number, number]): minMax { + const min = Math.min(A[0], A[2]), + max = Math.max(A[0], A[2]); + + if (A[1] > A[0] ? A[2] >= A[1] : A[2] <= A[1]) { + // if no extremum in ]0,1[ + return [min, max]; + } + + // check if the extremum E is min or max + const E = (A[0] * A[2] - A[1] * A[1]) / (A[0] - 2 * A[1] + A[2]); + return E < min ? [E, max] : [min, E]; +} + +// https://github.com/kpym/SVGPathy/blob/acd1a50c626b36d81969f6e98e8602e128ba4302/lib/box.js#L127 +function minmaxC(A: [number, number, number, number]): minMax { + const K = A[0] - 3 * A[1] + 3 * A[2] - A[3]; + + // if the polynomial is (almost) quadratic and not cubic + if (Math.abs(K) < CBEZIER_MINMAX_EPSILON) { + if (A[0] === A[3] && A[0] === A[1]) { + // no curve, point targeting same location + return [A[0], A[3]]; + } + + return minmaxQ([ + A[0], + -0.5 * A[0] + 1.5 * A[1], + A[0] - 3 * A[1] + 3 * A[2], + ]); + } + + // the reduced discriminant of the derivative + const T = + -A[0] * A[2] + + A[0] * A[3] - + A[1] * A[2] - + A[1] * A[3] + + A[1] * A[1] + + A[2] * A[2]; + + // if the polynomial is monotone in [0,1] + if (T <= 0) { + return [Math.min(A[0], A[3]), Math.max(A[0], A[3])]; + } + const S = Math.sqrt(T); + + // potential extrema + let min = Math.min(A[0], A[3]), + max = Math.max(A[0], A[3]); + + const L = A[0] - 2 * A[1] + A[2]; + // check local extrema + for (let R = (L + S) / K, i = 1; i <= 2; R = (L - S) / K, i++) { + if (R > 0 && R < 1) { + // if the extrema is for R in [0,1] + const Q = + A[0] * (1 - R) * (1 - R) * (1 - R) + + A[1] * 3 * (1 - R) * (1 - R) * R + + A[2] * 3 * (1 - R) * R * R + + A[3] * R * R * R; + if (Q < min) { + min = Q; + } + if (Q > max) { + max = Q; + } + } + } + + return [min, max]; +} + +/** + * Compute bounding boxes of SVG paths. + * @param {String} d SVG path for which their bounding box will be computed. + * @returns {BBox} + */ +export default function svgPathBbox(d: string): BBox { + const min = [Infinity, Infinity], + max = [-Infinity, -Infinity]; + svgPath(d) + .abs() + .unarc() + .unshort() + .iterate((seg, _, x, y) => { + switch (seg[0]) { + /*cases*/ + } + }, true); + return [min[0], min[1], max[0], max[1]]; +} diff --git a/package-lock.json b/package-lock.json index 1f5498f..9c2d21b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2196,14 +2196,20 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001298", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001298.tgz", - "integrity": "sha512-AcKqikjMLlvghZL/vfTHorlQsLDhGRalYf1+GmWCf5SCMziSGjRYQW/JEksj14NaYHIR6KIhrFAy0HV5C25UzQ==", + "version": "1.0.30001451", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001451.tgz", + "integrity": "sha512-XY7UbUpGRatZzoRft//5xOa69/1iGJRBlrieH6QYrkKLIFn3m7OVEJ81dSrKoy2BnKsdbX5cLrOispZNYo9v2w==", "dev": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - } + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + } + ] }, "node_modules/caseless": { "version": "0.12.0", @@ -10853,9 +10859,9 @@ "dev": true }, "caniuse-lite": { - "version": "1.0.30001298", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001298.tgz", - "integrity": "sha512-AcKqikjMLlvghZL/vfTHorlQsLDhGRalYf1+GmWCf5SCMziSGjRYQW/JEksj14NaYHIR6KIhrFAy0HV5C25UzQ==", + "version": "1.0.30001451", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001451.tgz", + "integrity": "sha512-XY7UbUpGRatZzoRft//5xOa69/1iGJRBlrieH6QYrkKLIFn3m7OVEJ81dSrKoy2BnKsdbX5cLrOispZNYo9v2w==", "dev": true }, "caseless": { diff --git a/package.json b/package.json index 83d5b9a..f7d785d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "svg-path-bbox", - "version": "1.2.3", + "version": "1.2.4", "description": "Compute bounding boxes of SVG paths.", "keywords": [ "svg", @@ -17,10 +17,11 @@ }, "scripts": { "coveralls": "cat ./tests/coverage/lcov.info | coveralls", - "prebuild": "run-s lint:fix dist:prepare", + "prebuild": "run-s build:index lint:fix dist:prepare", "build": "run-p build:ts build:cjs:wrapper", + "build:index": "ts-node scripts/build/index.ts", "build:ts": "tsc", - "build:cjs:wrapper": "ts-node scripts/build-cjs-wrapper.ts", + "build:cjs:wrapper": "ts-node scripts/build/cjs-wrapper.ts", "examples": "run-s example:*", "example:cjs": "node examples/common.js", "example:esm": "node examples/esm.mjs", diff --git a/scripts/build-cjs-wrapper.ts b/scripts/build-cjs-wrapper.ts deleted file mode 100644 index 8f94f7d..0000000 --- a/scripts/build-cjs-wrapper.ts +++ /dev/null @@ -1,6 +0,0 @@ -import * as fs from "node:fs"; - -fs.writeFileSync( - "dist/wrapper.js", - 'module.exports = require("./index.js").default;' -); diff --git a/scripts/build/cjs-wrapper.ts b/scripts/build/cjs-wrapper.ts new file mode 100644 index 0000000..c2f5652 --- /dev/null +++ b/scripts/build/cjs-wrapper.ts @@ -0,0 +1,8 @@ +import * as fs from "node:fs"; + +if (require.main === module) { + fs.writeFileSync( + "dist/wrapper.js", + 'module.exports = require("./index.js").default;' + ); +} diff --git a/scripts/build/index.ts b/scripts/build/index.ts new file mode 100644 index 0000000..253abae --- /dev/null +++ b/scripts/build/index.ts @@ -0,0 +1,26 @@ +import * as fs from "node:fs"; +import cases from "../../lib/cases"; +import { getSimpleIconsSegmentsStats } from "../simple-icons-segments-stats"; + +function buildIndexTs(indexTemplateTs: string): string { + let indexTs = ""; + const [beforeCases, afterCases] = indexTemplateTs.split("/*cases*/"); + indexTs += beforeCases.replace( + '"use strict";', + '"use strict";\n\n// WARNING: This file is autogenerated, edit lib/index.template.ts' + ); + const siSegmentsStats = getSimpleIconsSegmentsStats().map(([seg]) => seg); + for (const segment of siSegmentsStats) { + const caseBranch = cases[segment as keyof typeof cases]; + indexTs += `case "${segment}": ${caseBranch}\n `; + } + indexTs = indexTs.trimEnd(); + indexTs += afterCases; + return indexTs; +} + +if (require.main === module) { + const indexTemplateTs = fs.readFileSync("lib/index.template.ts", "utf8"); + const indexTs = buildIndexTs(indexTemplateTs); + fs.writeFileSync("src/index.ts", indexTs); +} diff --git a/scripts/simple-icons-segments-stats.ts b/scripts/simple-icons-segments-stats.ts new file mode 100644 index 0000000..9e59d35 --- /dev/null +++ b/scripts/simple-icons-segments-stats.ts @@ -0,0 +1,31 @@ +/** + * Shows how many segments of each type are found in + * simple-icons icons. + */ + +import * as icons from "simple-icons/icons"; + +export function getSimpleIconsSegmentsStats() { + const segmentsStats = { + M: 0, + V: 0, + H: 0, + L: 0, + C: 0, + Q: 0, + }; + Object.values(icons).map((icon) => { + for (const segment in segmentsStats) { + segmentsStats[segment as keyof typeof segmentsStats] += + icon.path.split(segment).length - 0; + } + }); + return Object.entries(segmentsStats).sort(([, a], [, b]) => b - a); +} + +if (require.main === module) { + const stats = getSimpleIconsSegmentsStats(); + for (const [seg, occ] of stats) { + process.stdout.write(`${seg}: ${occ}\n`); + } +} diff --git a/src/index.ts b/src/index.ts index 6dc7a23..8ef9a6b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,7 @@ "use strict"; +// WARNING: This file is autogenerated, edit lib/index.template.ts + import * as svgPath from "svgpath"; type minMax = [min: number, max: number]; @@ -96,8 +98,7 @@ export default function svgPathBbox(d: string): BBox { .unshort() .iterate((seg, _, x, y) => { switch (seg[0]) { - case "M": - case "L": { + case "M": { if (min[0] > seg[1]) { min[0] = seg[1]; } @@ -112,6 +113,15 @@ export default function svgPathBbox(d: string): BBox { } break; } + case "H": { + if (min[0] > seg[1]) { + min[0] = seg[1]; + } + if (max[0] < seg[1]) { + max[0] = seg[1]; + } + break; + } case "V": { if (min[1] > seg[1]) { min[1] = seg[1]; @@ -121,13 +131,19 @@ export default function svgPathBbox(d: string): BBox { } break; } - case "H": { + case "L": { if (min[0] > seg[1]) { min[0] = seg[1]; } + if (min[1] > seg[2]) { + min[1] = seg[2]; + } if (max[0] < seg[1]) { max[0] = seg[1]; } + if (max[1] < seg[2]) { + max[1] = seg[2]; + } break; } case "C": { diff --git a/tests/changelog.test.ts b/tests/changelog.test.ts index 5c1244d..9d034b3 100644 --- a/tests/changelog.test.ts +++ b/tests/changelog.test.ts @@ -6,6 +6,6 @@ test("Current version has a CHANGELOG entry", () => { const changelogEntry = changelog .split("\n") - .find((line: string) => line.startsWith(`## [${packageJson.version}]`)); + .find((line: string) => line.startsWith(`## ${packageJson.version}`)); expect(changelogEntry).toBeDefined(); }); diff --git a/tests/optimization.test.ts b/tests/optimization.test.ts new file mode 100644 index 0000000..d8f28e7 --- /dev/null +++ b/tests/optimization.test.ts @@ -0,0 +1,22 @@ +import * as fs from "node:fs"; +import { getSimpleIconsSegmentsStats } from "../scripts/simple-icons-segments-stats"; + +test("Segments parsing order switch is optimized", () => { + // We use simple-icons data to compute segment stats + const indexTs = fs.readFileSync("src/index.ts", "utf8"); + const siSegmentsByOccurrence = getSimpleIconsSegmentsStats().map( + ([seg]) => seg + ); + + const swithCasesSegmentByLines = indexTs + .split("\n") + .map((line) => { + if (line.startsWith(" case ")) { + return line.split('"')[1]; + } + return null; + }) + .filter((occ) => occ !== null); + + expect(siSegmentsByOccurrence).toStrictEqual(swithCasesSegmentByLines); +});