From d17ccee733e9707bfb205cac419992f7d992fce8 Mon Sep 17 00:00:00 2001 From: Kendell R Date: Thu, 23 Nov 2023 09:03:48 -0800 Subject: [PATCH 01/32] New plugin: optimize path order --- lib/builtin.js | 1 + plugins/optimizePathOrder.js | 246 +++++++++++++++++++++++++++++++++++ plugins/plugins-types.ts | 3 +- plugins/preset-default.js | 2 + 4 files changed, 251 insertions(+), 1 deletion(-) create mode 100644 plugins/optimizePathOrder.js diff --git a/lib/builtin.js b/lib/builtin.js index 28a380ec9..97845d278 100644 --- a/lib/builtin.js +++ b/lib/builtin.js @@ -23,6 +23,7 @@ exports.builtin = [ require('../plugins/minifyStyles.js'), require('../plugins/moveElemsAttrsToGroup.js'), require('../plugins/moveGroupAttrsToElems.js'), + require('../plugins/optimizePathOrder.js'), require('../plugins/prefixIds.js'), require('../plugins/removeAttributesBySelector.js'), require('../plugins/removeAttrs.js'), diff --git a/plugins/optimizePathOrder.js b/plugins/optimizePathOrder.js new file mode 100644 index 000000000..94e0bdbc6 --- /dev/null +++ b/plugins/optimizePathOrder.js @@ -0,0 +1,246 @@ +const { stringifyPathData } = require('../lib/path.js'); +const { computeStyle, collectStylesheet } = require('../lib/style.js'); +const { hasScripts } = require('../lib/svgo/tools.js'); +const { pathElems } = require('./_collections.js'); +const { path2js, js2path } = require('./_path.js'); + +exports.name = 'optimizePathOrder'; +exports.description = 'Moves around instructions in paths to be optimal.'; + +/** + * @typedef {import('../lib/types').PathDataCommand} PathDataCommand + * @typedef {import('../lib/types').PathDataItem} PathDataItem + * @typedef {{command: PathDataCommand, base: [number, number], coords: [number, number]}} InternalPath + * @typedef {InternalPath & {args: number[]}} RealPath + */ + +/** + * Moves around instructions in paths to be optimal. + * + * @author Kendell R + * + * @type {import('./plugins-types').Plugin<'optimizePathOrder'>} + */ +exports.fn = (root, params) => { + const stylesheet = collectStylesheet(root); + let deoptimized = false; + return { + element: { + enter: (node) => { + if (hasScripts(node)) { + deoptimized = true; + } + if (!pathElems.includes(node.name) || !node.attributes.d || deoptimized) + return; + + const computedStyle = computeStyle(stylesheet, node); + if ( + computedStyle['marker-start'] || + computedStyle['marker-mid'] || + computedStyle['marker-end'] || + computedStyle['stroke-dasharray'] + ) + return; + + const maybeHasStroke = + computedStyle.stroke && + (computedStyle.stroke.type === 'dynamic' || + computedStyle.stroke.value !== 'none'); + const unsafeToChangeStart = maybeHasStroke + ? computedStyle['stroke-linecap']?.type !== 'static' || + computedStyle['stroke-linecap'].value !== 'round' || + computedStyle['stroke-linejoin']?.type !== 'static' || + computedStyle['stroke-linejoin'].value !== 'round' + : false; + + const path = /** @type {RealPath[]} */ (path2js(node)); + + const parts = []; + let part = { valid: true, data: /** @type {RealPath[]} */ ([]) }; + for (const instruction of path) { + if (instruction.command == 'M' || instruction.command == 'm') { + if (part.data.length > 0) { + parts.push(part); + part = { valid: true, data: [] }; + } + } + if ( + instruction.command != 'm' && + instruction.command != 'M' && + instruction.command != 'l' && + instruction.command != 'L' && + instruction.command != 'h' && + instruction.command != 'H' && + instruction.command != 'v' && + instruction.command != 'V' && + instruction.command != 'z' && + instruction.command != 'Z' + ) { + part.valid = false; + } + part.data.push(instruction); + } + if (part.data.length > 0) { + parts.push(part); + } + + /** + * @type {PathDataItem[]} + */ + const pathTransformed = []; + for (const part of parts) { + if (part.valid) { + const internalData = part.data.filter( + (item) => item.command != 'm' && item.command != 'M' + ); + if (internalData.length > 0) { + const start = internalData[0].base; + const end = internalData[internalData.length - 1].coords; + pathTransformed.push( + ...optimizePart( + internalData.map((item) => { + return { + command: item.command, + base: item.base, + coords: item.coords, + }; + }), + part.data, + unsafeToChangeStart || + start[0] != end[0] || + start[1] != end[1] + ) + ); + continue; + } + } + pathTransformed.push(...part.data); + } + + js2path(node, pathTransformed, {}); + }, + }, + }; +}; + +/** + * @param {InternalPath[]} path + * @param {PathDataItem[]} baseline + * @param {boolean} unsafeToChangeStart + */ +function optimizePart(path, baseline, unsafeToChangeStart) { + const starts = unsafeToChangeStart + ? [0] + : Array.from({ length: path.length }, (_, i) => i); + let best = { + size: stringifyPathData({ pathData: baseline }).length, + data: baseline, + }; + for (const start of starts) { + for (const reverse of [false, true]) { + const data = reverse + ? path + .slice(0, start) + .reverse() + .concat(path.slice(start).reverse()) + .map((item) => { + return { + command: item.command, + base: item.coords, + coords: item.base, + }; + }) + : path.slice(start).concat(path.slice(0, start)); + + /** + * @type {InternalPath[]} + */ + const output = []; + output.push({ + command: 'M', + base: [0, 0], + coords: [data[0].base[0], data[0].base[1]], + }); + for (const item of data) { + output.push({ + command: 'L', + base: [item.base[0], item.base[1]], + coords: [item.coords[0], item.coords[1]], + }); + } + + const outputPath = transformPath(output, !unsafeToChangeStart); + const size = stringifyPathData({ pathData: outputPath }).length; + if (size < best.size) { + best = { size, data: outputPath }; + } + } + } + return best.data; +} + +/** + * @param {InternalPath[]} path + * @param {boolean} canUseZ + */ +function transformPath(path, canUseZ) { + return path.reduce( + ( + /** @type {PathDataItem[]} */ acc, + /** @type {InternalPath} */ command, + /** @type {number} */ i + ) => { + const lastCommand = acc[acc.length - 1]?.command; + + if (command.command == 'M') + acc.push({ command: 'M', args: command.coords }); + else if (command.command == 'L') { + const relativeX = command.coords[0] - command.base[0]; + const relativeY = command.coords[1] - command.base[1]; + if (i == path.length - 1 && canUseZ) { + acc.push({ command: 'z', args: [] }); + } else if (command.base[1] == command.coords[1]) { + const isAbsoluteBetter = + command.coords[0].toString().length < relativeX.toString().length; + acc.push( + isAbsoluteBetter + ? { command: 'H', args: [command.coords[0]] } + : { command: 'h', args: [relativeX] } + ); + } else if (command.base[0] == command.coords[0]) { + const isAbsoluteBetter = + command.coords[1].toString().length < relativeY.toString().length; + acc.push( + isAbsoluteBetter + ? { command: 'V', args: [command.coords[1]] } + : { command: 'v', args: [relativeY] } + ); + } else { + const absoluteLength = + command.coords[0].toString().length + + command.coords[1].toString().length + + (command.coords[1] < 0 ? 0 : 1) + + lastCommand == + 'L' + ? 0 + : 1; + const relativeLength = + relativeX.toString().length + + relativeY.toString().length + + (relativeY < 0 ? 0 : 1) + + lastCommand == + 'l' + ? 0 + : 1; + acc.push( + absoluteLength < relativeLength + ? { command: 'L', args: command.coords } + : { command: 'l', args: [relativeX, relativeY] } + ); + } + } + return acc; + }, + [] + ); +} diff --git a/plugins/plugins-types.ts b/plugins/plugins-types.ts index 8e3972c1c..537fd0105 100644 --- a/plugins/plugins-types.ts +++ b/plugins/plugins-types.ts @@ -138,6 +138,7 @@ type DefaultPlugins = { moveElemsAttrsToGroup: void; moveGroupAttrsToElems: void; + optimizePathOrder: void; removeComments: { preservePatterns: Array | false; }; @@ -249,7 +250,7 @@ export type BuiltinsWithOptionalParams = DefaultPlugins & { * * @default false */ - includeLegacy: boolean + includeLegacy: boolean; }; removeXMLNS: void; reusePaths: void; diff --git a/plugins/preset-default.js b/plugins/preset-default.js index 86516fd72..fab6c564f 100644 --- a/plugins/preset-default.js +++ b/plugins/preset-default.js @@ -28,6 +28,7 @@ const moveElemsAttrsToGroup = require('./moveElemsAttrsToGroup.js'); const moveGroupAttrsToElems = require('./moveGroupAttrsToElems.js'); const collapseGroups = require('./collapseGroups.js'); const convertPathData = require('./convertPathData.js'); +const optimizePathOrder = require('./optimizePathOrder.js'); const convertTransform = require('./convertTransform.js'); const removeEmptyAttrs = require('./removeEmptyAttrs.js'); const removeEmptyContainers = require('./removeEmptyContainers.js'); @@ -67,6 +68,7 @@ const presetDefault = createPreset({ moveGroupAttrsToElems, collapseGroups, convertPathData, + optimizePathOrder, convertTransform, removeEmptyAttrs, removeEmptyContainers, From 37fe2d6ae2ce0ad51caaae172791f39b6474fcdb Mon Sep 17 00:00:00 2001 From: Kendell R Date: Thu, 23 Nov 2023 09:28:54 -0800 Subject: [PATCH 02/32] properly account for precision --- plugins/optimizePathOrder.js | 38 ++++++++++++++++++++++++------------ plugins/plugins-types.ts | 5 ++++- 2 files changed, 29 insertions(+), 14 deletions(-) diff --git a/plugins/optimizePathOrder.js b/plugins/optimizePathOrder.js index 94e0bdbc6..dc2c2a976 100644 --- a/plugins/optimizePathOrder.js +++ b/plugins/optimizePathOrder.js @@ -22,6 +22,7 @@ exports.description = 'Moves around instructions in paths to be optimal.'; * @type {import('./plugins-types').Plugin<'optimizePathOrder'>} */ exports.fn = (root, params) => { + const { floatPrecision: precision = 3, noSpaceAfterFlags } = params; const stylesheet = collectStylesheet(root); let deoptimized = false; return { @@ -97,19 +98,21 @@ exports.fn = (root, params) => { const start = internalData[0].base; const end = internalData[internalData.length - 1].coords; pathTransformed.push( - ...optimizePart( - internalData.map((item) => { + ...optimizePart({ + path: internalData.map((item) => { return { command: item.command, base: item.base, coords: item.coords, }; }), - part.data, - unsafeToChangeStart || + baseline: part.data, + unsafeToChangeStart: + unsafeToChangeStart || start[0] != end[0] || - start[1] != end[1] - ) + start[1] != end[1], + precision, + }) ); continue; } @@ -117,23 +120,29 @@ exports.fn = (root, params) => { pathTransformed.push(...part.data); } - js2path(node, pathTransformed, {}); + js2path(node, pathTransformed, { + floatPrecision: precision, + noSpaceAfterFlags, + }); }, }, }; }; /** - * @param {InternalPath[]} path - * @param {PathDataItem[]} baseline - * @param {boolean} unsafeToChangeStart + * @param {{ + * path: InternalPath[], + * baseline: PathDataItem[], + * unsafeToChangeStart: boolean, + * precision: number | undefined + * }} param0 */ -function optimizePart(path, baseline, unsafeToChangeStart) { +function optimizePart({ path, baseline, unsafeToChangeStart, precision }) { const starts = unsafeToChangeStart ? [0] : Array.from({ length: path.length }, (_, i) => i); let best = { - size: stringifyPathData({ pathData: baseline }).length, + size: stringifyPathData({ pathData: baseline, precision }).length, data: baseline, }; for (const start of starts) { @@ -170,7 +179,10 @@ function optimizePart(path, baseline, unsafeToChangeStart) { } const outputPath = transformPath(output, !unsafeToChangeStart); - const size = stringifyPathData({ pathData: outputPath }).length; + const size = stringifyPathData({ + pathData: outputPath, + precision, + }).length; if (size < best.size) { best = { size, data: outputPath }; } diff --git a/plugins/plugins-types.ts b/plugins/plugins-types.ts index 537fd0105..d43ab2d17 100644 --- a/plugins/plugins-types.ts +++ b/plugins/plugins-types.ts @@ -138,7 +138,10 @@ type DefaultPlugins = { moveElemsAttrsToGroup: void; moveGroupAttrsToElems: void; - optimizePathOrder: void; + optimizePathOrder: { + floatPrecision?: number; + noSpaceAfterFlags?: boolean; + }; removeComments: { preservePatterns: Array | false; }; From 1a3ee7c987f98f152dc4d3531a92f6b30a7e0393 Mon Sep 17 00:00:00 2001 From: Kendell R Date: Thu, 23 Nov 2023 11:11:49 -0800 Subject: [PATCH 03/32] update logic to account for more stuff --- plugins/optimizePathOrder.js | 223 ++++++++++++++++++++++++++--------- 1 file changed, 166 insertions(+), 57 deletions(-) diff --git a/plugins/optimizePathOrder.js b/plugins/optimizePathOrder.js index dc2c2a976..c96dff22a 100644 --- a/plugins/optimizePathOrder.js +++ b/plugins/optimizePathOrder.js @@ -1,6 +1,6 @@ const { stringifyPathData } = require('../lib/path.js'); const { computeStyle, collectStylesheet } = require('../lib/style.js'); -const { hasScripts } = require('../lib/svgo/tools.js'); +const { hasScripts, cleanupOutData } = require('../lib/svgo/tools.js'); const { pathElems } = require('./_collections.js'); const { path2js, js2path } = require('./_path.js'); @@ -53,6 +53,10 @@ exports.fn = (root, params) => { computedStyle['stroke-linejoin']?.type !== 'static' || computedStyle['stroke-linejoin'].value !== 'round' : false; + const unsafeToChangeDirection = computedStyle.fill + ? computedStyle['fill-rule']?.type !== 'static' || + computedStyle['fill-rule'].value == 'nonzero' + : false; const path = /** @type {RealPath[]} */ (path2js(node)); @@ -89,7 +93,7 @@ exports.fn = (root, params) => { * @type {PathDataItem[]} */ const pathTransformed = []; - for (const part of parts) { + for (const [i, part] of parts.entries()) { if (part.valid) { const internalData = part.data.filter( (item) => item.command != 'm' && item.command != 'M' @@ -97,26 +101,32 @@ exports.fn = (root, params) => { if (internalData.length > 0) { const start = internalData[0].base; const end = internalData[internalData.length - 1].coords; - pathTransformed.push( - ...optimizePart({ - path: internalData.map((item) => { - return { - command: item.command, - base: item.base, - coords: item.coords, - }; - }), - baseline: part.data, - unsafeToChangeStart: - unsafeToChangeStart || - start[0] != end[0] || - start[1] != end[1], - precision, - }) - ); - continue; + const next = parts[i + 1]; + + const result = optimizePart({ + path: internalData.map((item) => { + return { + command: item.command, + base: item.base, + coords: item.coords, + }; + }), + unsafeToChangeStart: + unsafeToChangeStart || + start[0] != end[0] || + start[1] != end[1], + unsafeToChangeDirection, + next: next?.data[0].command == 'm' ? next.data[0] : undefined, + baseline: part.data, + precision, + }); + if (result.success) { + pathTransformed.push(...result.data); + continue; + } } } + pathTransformed.push(...part.data); } @@ -132,21 +142,33 @@ exports.fn = (root, params) => { /** * @param {{ * path: InternalPath[], - * baseline: PathDataItem[], * unsafeToChangeStart: boolean, - * precision: number | undefined + * unsafeToChangeDirection: boolean, + * next: RealPath | undefined, + * baseline: RealPath[], + * precision: number * }} param0 */ -function optimizePart({ path, baseline, unsafeToChangeStart, precision }) { +function optimizePart({ + path, + unsafeToChangeStart, + unsafeToChangeDirection, + next, + baseline, + precision, +}) { const starts = unsafeToChangeStart ? [0] : Array.from({ length: path.length }, (_, i) => i); let best = { - size: stringifyPathData({ pathData: baseline, precision }).length, + success: false, + size: + stringifyPathData({ pathData: baseline, precision }).length + + (next ? estimateLength(next.args, precision) : 0), data: baseline, }; for (const start of starts) { - for (const reverse of [false, true]) { + for (const reverse of unsafeToChangeDirection ? [false] : [false, true]) { const data = reverse ? path .slice(0, start) @@ -178,76 +200,135 @@ function optimizePart({ path, baseline, unsafeToChangeStart, precision }) { }); } - const outputPath = transformPath(output, !unsafeToChangeStart); - const size = stringifyPathData({ - pathData: outputPath, - precision, - }).length; + const outputPath = transformPath(output, precision, !unsafeToChangeStart); + const size = + stringifyPathData({ + pathData: outputPath, + precision, + }).length + + (next + ? estimateLength( + transformMove(next, output[output.length - 1].coords), + precision + ) + : 0); if (size < best.size) { - best = { size, data: outputPath }; + outputPath.forEach( + (item) => + (item.args = item.args.map((a) => toPrecision(a, precision))) + ); + if (next) + next.args = transformMove(next, output[output.length - 1].coords); + best = { + success: true, + size, + data: outputPath, + }; } } } - return best.data; + return best; } /** * @param {InternalPath[]} path + * @param {number} precision * @param {boolean} canUseZ */ -function transformPath(path, canUseZ) { +function transformPath(path, precision, canUseZ) { return path.reduce( ( - /** @type {PathDataItem[]} */ acc, + /** @type {RealPath[]} */ acc, /** @type {InternalPath} */ command, /** @type {number} */ i ) => { const lastCommand = acc[acc.length - 1]?.command; if (command.command == 'M') - acc.push({ command: 'M', args: command.coords }); + acc.push({ + command: 'M', + args: command.coords, + base: command.base, + coords: command.coords, + }); else if (command.command == 'L') { const relativeX = command.coords[0] - command.base[0]; const relativeY = command.coords[1] - command.base[1]; if (i == path.length - 1 && canUseZ) { - acc.push({ command: 'z', args: [] }); + acc.push({ + command: 'z', + args: [], + base: command.base, + coords: command.coords, + }); } else if (command.base[1] == command.coords[1]) { - const isAbsoluteBetter = - command.coords[0].toString().length < relativeX.toString().length; + const absoluteLength = toPrecision( + command.coords[0], + precision + ).toString().length; + const relativeLength = toPrecision(relativeX, precision).toString() + .length; acc.push( - isAbsoluteBetter - ? { command: 'H', args: [command.coords[0]] } - : { command: 'h', args: [relativeX] } + absoluteLength < relativeLength + ? { + command: 'H', + args: [command.coords[0]], + base: command.base, + coords: command.coords, + } + : { + command: 'h', + args: [relativeX], + base: command.base, + coords: command.coords, + } ); } else if (command.base[0] == command.coords[0]) { - const isAbsoluteBetter = - command.coords[1].toString().length < relativeY.toString().length; + const absoluteLength = toPrecision( + command.coords[1], + precision + ).toString().length; + const relativeLength = toPrecision(relativeY, precision).toString() + .length; acc.push( - isAbsoluteBetter - ? { command: 'V', args: [command.coords[1]] } - : { command: 'v', args: [relativeY] } + absoluteLength < relativeLength + ? { + command: 'V', + args: [command.coords[1]], + base: command.base, + coords: command.coords, + } + : { + command: 'v', + args: [relativeY], + base: command.base, + coords: command.coords, + } ); } else { const absoluteLength = - command.coords[0].toString().length + - command.coords[1].toString().length + - (command.coords[1] < 0 ? 0 : 1) + - lastCommand == - 'L' + estimateLength(command.coords, precision) + lastCommand == 'L' ? 0 : 1; const relativeLength = - relativeX.toString().length + - relativeY.toString().length + - (relativeY < 0 ? 0 : 1) + - lastCommand == + estimateLength([relativeX, relativeY], precision) + lastCommand == 'l' ? 0 : 1; acc.push( absoluteLength < relativeLength - ? { command: 'L', args: command.coords } - : { command: 'l', args: [relativeX, relativeY] } + ? { + command: 'L', + args: command.coords, + base: command.base, + coords: command.coords, + } + : { + command: 'l', + args: [relativeX, relativeY], + base: command.base, + coords: command.coords, + } ); } } @@ -256,3 +337,31 @@ function transformPath(path, canUseZ) { [] ); } + +/** + * @param {RealPath} command + * @param {[number, number]} newBase + */ +function transformMove(command, newBase) { + return [command.coords[0] - newBase[0], command.coords[1] - newBase[1]]; +} + +/** + * @param {number} number + * @param {number} precision + */ +function toPrecision(number, precision) { + const factor = Math.pow(10, precision); + return Math.round(number * factor) / factor; +} + +/** + * @param {number[]} numbers + * @param {number} precision + */ +function estimateLength(numbers, precision) { + return cleanupOutData( + numbers.map((n) => toPrecision(n, precision)), + {} + ).length; +} From e3059667e08b12d21066c6a29a3b88d5d9312f14 Mon Sep 17 00:00:00 2001 From: Kendell R Date: Thu, 23 Nov 2023 11:35:38 -0800 Subject: [PATCH 04/32] implement arcs --- plugins/optimizePathOrder.js | 159 ++++++++++++++++++++++------------- 1 file changed, 99 insertions(+), 60 deletions(-) diff --git a/plugins/optimizePathOrder.js b/plugins/optimizePathOrder.js index c96dff22a..61d1c1766 100644 --- a/plugins/optimizePathOrder.js +++ b/plugins/optimizePathOrder.js @@ -10,8 +10,8 @@ exports.description = 'Moves around instructions in paths to be optimal.'; /** * @typedef {import('../lib/types').PathDataCommand} PathDataCommand * @typedef {import('../lib/types').PathDataItem} PathDataItem - * @typedef {{command: PathDataCommand, base: [number, number], coords: [number, number]}} InternalPath - * @typedef {InternalPath & {args: number[]}} RealPath + * @typedef {{command: PathDataCommand, data?: {rx: number, ry: number, r: number, large: boolean, sweep: boolean}, base: [number, number], coords: [number, number]}} InternalPath + * @typedef {PathDataItem & {base: [number, number], coords: [number, number]}} RealPath */ /** @@ -78,6 +78,8 @@ exports.fn = (root, params) => { instruction.command != 'H' && instruction.command != 'v' && instruction.command != 'V' && + instruction.command != 'a' && + instruction.command != 'A' && instruction.command != 'z' && instruction.command != 'Z' ) { @@ -104,13 +106,21 @@ exports.fn = (root, params) => { const next = parts[i + 1]; const result = optimizePart({ - path: internalData.map((item) => { - return { - command: item.command, - base: item.base, - coords: item.coords, - }; - }), + path: internalData.map((item) => ({ + command: item.command, + data: + item.command == 'a' || item.command == 'A' + ? { + rx: item.args[0], + ry: item.args[1], + r: item.args[2], + large: Boolean(item.args[3]), + sweep: Boolean(item.args[4]), + } + : undefined, + base: item.base, + coords: item.coords, + })), unsafeToChangeStart: unsafeToChangeStart || start[0] != end[0] || @@ -177,6 +187,10 @@ function optimizePart({ .map((item) => { return { command: item.command, + data: item.data && { + ...item.data, + sweep: !item.data.sweep, + }, base: item.coords, coords: item.base, }; @@ -193,11 +207,20 @@ function optimizePart({ coords: [data[0].base[0], data[0].base[1]], }); for (const item of data) { - output.push({ - command: 'L', - base: [item.base[0], item.base[1]], - coords: [item.coords[0], item.coords[1]], - }); + if (item.command == 'a' || item.command == 'A') { + output.push({ + command: 'A', + data: item.data, + base: item.base, + coords: item.coords, + }); + } else { + output.push({ + command: 'L', + base: item.base, + coords: item.coords, + }); + } } const outputPath = transformPath(output, precision, !unsafeToChangeStart); @@ -251,7 +274,41 @@ function transformPath(path, precision, canUseZ) { base: command.base, coords: command.coords, }); - else if (command.command == 'L') { + else if (command.command == 'A') { + const data = /** @type {Exclude} */ ( + command.data + ); + const args = [ + data.rx, + data.ry, + data.r, + data.large ? 1 : 0, + data.sweep ? 1 : 0, + ]; + const relativeX = command.coords[0] - command.base[0]; + const relativeY = command.coords[1] - command.base[1]; + const absoluteLength = + estimateLength([...args, ...command.coords], precision) + + lastCommand == + 'A' + ? 0 + : 1; + const relativeLength = + estimateLength([...args, relativeX, relativeY], precision) + + lastCommand == + 'a' + ? 0 + : 1; + acc.push({ + command: absoluteLength < relativeLength ? 'A' : 'a', + args: + absoluteLength < relativeLength + ? [...args, ...command.coords] + : [...args, relativeX, relativeY], + base: command.base, + coords: command.coords, + }); + } else if (command.command == 'L') { const relativeX = command.coords[0] - command.base[0]; const relativeY = command.coords[1] - command.base[1]; if (i == path.length - 1 && canUseZ) { @@ -268,21 +325,15 @@ function transformPath(path, precision, canUseZ) { ).toString().length; const relativeLength = toPrecision(relativeX, precision).toString() .length; - acc.push( - absoluteLength < relativeLength - ? { - command: 'H', - args: [command.coords[0]], - base: command.base, - coords: command.coords, - } - : { - command: 'h', - args: [relativeX], - base: command.base, - coords: command.coords, - } - ); + acc.push({ + command: absoluteLength < relativeLength ? 'H' : 'h', + args: + absoluteLength < relativeLength + ? [command.coords[0]] + : [relativeX], + base: command.base, + coords: command.coords, + }); } else if (command.base[0] == command.coords[0]) { const absoluteLength = toPrecision( command.coords[1], @@ -290,21 +341,15 @@ function transformPath(path, precision, canUseZ) { ).toString().length; const relativeLength = toPrecision(relativeY, precision).toString() .length; - acc.push( - absoluteLength < relativeLength - ? { - command: 'V', - args: [command.coords[1]], - base: command.base, - coords: command.coords, - } - : { - command: 'v', - args: [relativeY], - base: command.base, - coords: command.coords, - } - ); + acc.push({ + command: absoluteLength < relativeLength ? 'V' : 'v', + args: + absoluteLength < relativeLength + ? [command.coords[1]] + : [relativeY], + base: command.base, + coords: command.coords, + }); } else { const absoluteLength = estimateLength(command.coords, precision) + lastCommand == 'L' @@ -315,21 +360,15 @@ function transformPath(path, precision, canUseZ) { 'l' ? 0 : 1; - acc.push( - absoluteLength < relativeLength - ? { - command: 'L', - args: command.coords, - base: command.base, - coords: command.coords, - } - : { - command: 'l', - args: [relativeX, relativeY], - base: command.base, - coords: command.coords, - } - ); + acc.push({ + command: absoluteLength < relativeLength ? 'L' : 'l', + args: + absoluteLength < relativeLength + ? command.coords + : [relativeX, relativeY], + base: command.base, + coords: command.coords, + }); } } return acc; From 913f27c90ef47297e3d4ecc435d8d6d1f64c5810 Mon Sep 17 00:00:00 2001 From: Kendell R Date: Thu, 23 Nov 2023 11:45:27 -0800 Subject: [PATCH 05/32] add a doc page --- docs/03-plugins/optimize-path-order.mdx | 31 +++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 docs/03-plugins/optimize-path-order.mdx diff --git a/docs/03-plugins/optimize-path-order.mdx b/docs/03-plugins/optimize-path-order.mdx new file mode 100644 index 000000000..d70b1d63b --- /dev/null +++ b/docs/03-plugins/optimize-path-order.mdx @@ -0,0 +1,31 @@ +--- +title: Optimize Path Order +svgo: + pluginId: optimizePathOrder + defaultPlugin: true + parameters: + floatPrecision: + description: Number of decimal places to round to, using conventional rounding rules. + default: 3 + noSpaceAfterFlags: + description: If to omit spaces after flags. Flags are values that can only be 0 or 1 and are used by some path commands, namely A and a. + default: false +--- + +Optimizes parts of paths by starting in different places and reversing them. + +## Usage + + + +### Parameters + + + +## Demo + + + +## Implementation + +* https://github.com/svg/svgo/blob/main/plugins/optimizePathOrder.js From 70071043c949254a9cb92719a57616ffed8faad2 Mon Sep 17 00:00:00 2001 From: Kendell R Date: Thu, 23 Nov 2023 11:45:34 -0800 Subject: [PATCH 06/32] rename data to arc --- plugins/optimizePathOrder.js | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/plugins/optimizePathOrder.js b/plugins/optimizePathOrder.js index 61d1c1766..473b8e624 100644 --- a/plugins/optimizePathOrder.js +++ b/plugins/optimizePathOrder.js @@ -10,7 +10,7 @@ exports.description = 'Moves around instructions in paths to be optimal.'; /** * @typedef {import('../lib/types').PathDataCommand} PathDataCommand * @typedef {import('../lib/types').PathDataItem} PathDataItem - * @typedef {{command: PathDataCommand, data?: {rx: number, ry: number, r: number, large: boolean, sweep: boolean}, base: [number, number], coords: [number, number]}} InternalPath + * @typedef {{command: PathDataCommand, arc?: {rx: number, ry: number, r: number, large: boolean, sweep: boolean}, base: [number, number], coords: [number, number]}} InternalPath * @typedef {PathDataItem & {base: [number, number], coords: [number, number]}} RealPath */ @@ -108,7 +108,7 @@ exports.fn = (root, params) => { const result = optimizePart({ path: internalData.map((item) => ({ command: item.command, - data: + arc: item.command == 'a' || item.command == 'A' ? { rx: item.args[0], @@ -187,9 +187,9 @@ function optimizePart({ .map((item) => { return { command: item.command, - data: item.data && { - ...item.data, - sweep: !item.data.sweep, + arc: item.arc && { + ...item.arc, + sweep: !item.arc.sweep, }, base: item.coords, coords: item.base, @@ -210,7 +210,7 @@ function optimizePart({ if (item.command == 'a' || item.command == 'A') { output.push({ command: 'A', - data: item.data, + arc: item.arc, base: item.base, coords: item.coords, }); @@ -275,9 +275,10 @@ function transformPath(path, precision, canUseZ) { coords: command.coords, }); else if (command.command == 'A') { - const data = /** @type {Exclude} */ ( - command.data - ); + const data = + /** @type {{rx: number, ry: number, r: number, large: boolean, sweep: boolean}} */ ( + command.arc + ); const args = [ data.rx, data.ry, From 192d29ca097fbfdfc5bc396adadddb96c622d2ae Mon Sep 17 00:00:00 2001 From: Kendell R Date: Thu, 23 Nov 2023 11:46:45 -0800 Subject: [PATCH 07/32] less hacky way of reducing precision --- plugins/mergePaths.js | 2 +- plugins/optimizePathOrder.js | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/plugins/mergePaths.js b/plugins/mergePaths.js index 05c7ac705..226a5e59e 100644 --- a/plugins/mergePaths.js +++ b/plugins/mergePaths.js @@ -17,7 +17,7 @@ exports.description = 'merges multiple paths in one if possible'; exports.fn = (root, params) => { const { force = false, - floatPrecision, + floatPrecision = 3, noSpaceAfterFlags = false, // a20 60 45 0 1 30 20 → a20 60 45 0130 20 } = params; const stylesheet = collectStylesheet(root); diff --git a/plugins/optimizePathOrder.js b/plugins/optimizePathOrder.js index 473b8e624..1b4c8c59e 100644 --- a/plugins/optimizePathOrder.js +++ b/plugins/optimizePathOrder.js @@ -236,10 +236,6 @@ function optimizePart({ ) : 0); if (size < best.size) { - outputPath.forEach( - (item) => - (item.args = item.args.map((a) => toPrecision(a, precision))) - ); if (next) next.args = transformMove(next, output[output.length - 1].coords); best = { From 68b3c348d66623e14b7263cd1808f2a94ef099d4 Mon Sep 17 00:00:00 2001 From: Kendell R Date: Thu, 23 Nov 2023 16:20:27 -0800 Subject: [PATCH 08/32] Fix ternaries --- plugins/optimizePathOrder.js | 21 ++++++--------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/plugins/optimizePathOrder.js b/plugins/optimizePathOrder.js index 1b4c8c59e..ee4479e2b 100644 --- a/plugins/optimizePathOrder.js +++ b/plugins/optimizePathOrder.js @@ -286,16 +286,10 @@ function transformPath(path, precision, canUseZ) { const relativeY = command.coords[1] - command.base[1]; const absoluteLength = estimateLength([...args, ...command.coords], precision) + - lastCommand == - 'A' - ? 0 - : 1; + (lastCommand == 'A' ? 0 : 1); const relativeLength = estimateLength([...args, relativeX, relativeY], precision) + - lastCommand == - 'a' - ? 0 - : 1; + (lastCommand == 'a' ? 0 : 1); acc.push({ command: absoluteLength < relativeLength ? 'A' : 'a', args: @@ -349,14 +343,11 @@ function transformPath(path, precision, canUseZ) { }); } else { const absoluteLength = - estimateLength(command.coords, precision) + lastCommand == 'L' - ? 0 - : 1; + estimateLength(command.coords, precision) + + (lastCommand == 'L' ? 0 : 1); const relativeLength = - estimateLength([relativeX, relativeY], precision) + lastCommand == - 'l' - ? 0 - : 1; + estimateLength([relativeX, relativeY], precision) + + (lastCommand == 'l' ? 0 : 1); acc.push({ command: absoluteLength < relativeLength ? 'L' : 'l', args: From e31afa29c021f341b3b2ac5bd0829ffd29377a70 Mon Sep 17 00:00:00 2001 From: Kendell R Date: Fri, 24 Nov 2023 07:04:52 -0800 Subject: [PATCH 09/32] switch to a for loop --- plugins/optimizePathOrder.js | 25 +++++++++---------------- 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/plugins/optimizePathOrder.js b/plugins/optimizePathOrder.js index ee4479e2b..da9f380d5 100644 --- a/plugins/optimizePathOrder.js +++ b/plugins/optimizePathOrder.js @@ -255,12 +255,11 @@ function optimizePart({ * @param {boolean} canUseZ */ function transformPath(path, precision, canUseZ) { - return path.reduce( - ( - /** @type {RealPath[]} */ acc, - /** @type {InternalPath} */ command, - /** @type {number} */ i - ) => { + /** + * @type {RealPath[]} + */ + const acc = []; + for (const command of path) { const lastCommand = acc[acc.length - 1]?.command; if (command.command == 'M') @@ -302,7 +301,7 @@ function transformPath(path, precision, canUseZ) { } else if (command.command == 'L') { const relativeX = command.coords[0] - command.base[0]; const relativeY = command.coords[1] - command.base[1]; - if (i == path.length - 1 && canUseZ) { + if (acc.length == path.length - 1 && canUseZ) { acc.push({ command: 'z', args: [], @@ -319,9 +318,7 @@ function transformPath(path, precision, canUseZ) { acc.push({ command: absoluteLength < relativeLength ? 'H' : 'h', args: - absoluteLength < relativeLength - ? [command.coords[0]] - : [relativeX], + absoluteLength < relativeLength ? [command.coords[0]] : [relativeX], base: command.base, coords: command.coords, }); @@ -335,9 +332,7 @@ function transformPath(path, precision, canUseZ) { acc.push({ command: absoluteLength < relativeLength ? 'V' : 'v', args: - absoluteLength < relativeLength - ? [command.coords[1]] - : [relativeY], + absoluteLength < relativeLength ? [command.coords[1]] : [relativeY], base: command.base, coords: command.coords, }); @@ -357,12 +352,10 @@ function transformPath(path, precision, canUseZ) { base: command.base, coords: command.coords, }); + } } } return acc; - }, - [] - ); } /** From 5bfef70755d4f2031c3e170b65dc05b1126c38b0 Mon Sep 17 00:00:00 2001 From: Kendell R Date: Fri, 24 Nov 2023 07:16:59 -0800 Subject: [PATCH 10/32] fix formatting & use a faster, better stringifyargs --- lib/path.js | 1 + plugins/optimizePathOrder.js | 173 +++++++++++++++++------------------ 2 files changed, 86 insertions(+), 88 deletions(-) diff --git a/lib/path.js b/lib/path.js index c8782ce80..d959c7c5b 100644 --- a/lib/path.js +++ b/lib/path.js @@ -346,4 +346,5 @@ const stringifyPathData = ({ pathData, precision, disableSpaceAfterFlags }) => { } return result; }; +exports.stringifyArgs = stringifyArgs; exports.stringifyPathData = stringifyPathData; diff --git a/plugins/optimizePathOrder.js b/plugins/optimizePathOrder.js index da9f380d5..8ecd6cfa0 100644 --- a/plugins/optimizePathOrder.js +++ b/plugins/optimizePathOrder.js @@ -1,6 +1,6 @@ -const { stringifyPathData } = require('../lib/path.js'); +const { stringifyPathData, stringifyArgs } = require('../lib/path.js'); const { computeStyle, collectStylesheet } = require('../lib/style.js'); -const { hasScripts, cleanupOutData } = require('../lib/svgo/tools.js'); +const { hasScripts } = require('../lib/svgo/tools.js'); const { pathElems } = require('./_collections.js'); const { path2js, js2path } = require('./_path.js'); @@ -260,102 +260,102 @@ function transformPath(path, precision, canUseZ) { */ const acc = []; for (const command of path) { - const lastCommand = acc[acc.length - 1]?.command; + const lastCommand = acc[acc.length - 1]?.command; - if (command.command == 'M') + if (command.command == 'M') + acc.push({ + command: 'M', + args: command.coords, + base: command.base, + coords: command.coords, + }); + else if (command.command == 'A') { + const data = + /** @type {{rx: number, ry: number, r: number, large: boolean, sweep: boolean}} */ ( + command.arc + ); + const args = [ + data.rx, + data.ry, + data.r, + data.large ? 1 : 0, + data.sweep ? 1 : 0, + ]; + const relativeX = command.coords[0] - command.base[0]; + const relativeY = command.coords[1] - command.base[1]; + const absoluteLength = + estimateLength([...args, ...command.coords], precision) + + (lastCommand == 'A' ? 0 : 1); + const relativeLength = + estimateLength([...args, relativeX, relativeY], precision) + + (lastCommand == 'a' ? 0 : 1); + acc.push({ + command: absoluteLength < relativeLength ? 'A' : 'a', + args: + absoluteLength < relativeLength + ? [...args, ...command.coords] + : [...args, relativeX, relativeY], + base: command.base, + coords: command.coords, + }); + } else if (command.command == 'L') { + const relativeX = command.coords[0] - command.base[0]; + const relativeY = command.coords[1] - command.base[1]; + if (acc.length == path.length - 1 && canUseZ) { + acc.push({ + command: 'z', + args: [], + base: command.base, + coords: command.coords, + }); + } else if (command.base[1] == command.coords[1]) { + const absoluteLength = toPrecision( + command.coords[0], + precision + ).toString().length; + const relativeLength = toPrecision(relativeX, precision).toString() + .length; + acc.push({ + command: absoluteLength < relativeLength ? 'H' : 'h', + args: + absoluteLength < relativeLength ? [command.coords[0]] : [relativeX], + base: command.base, + coords: command.coords, + }); + } else if (command.base[0] == command.coords[0]) { + const absoluteLength = toPrecision( + command.coords[1], + precision + ).toString().length; + const relativeLength = toPrecision(relativeY, precision).toString() + .length; acc.push({ - command: 'M', - args: command.coords, + command: absoluteLength < relativeLength ? 'V' : 'v', + args: + absoluteLength < relativeLength ? [command.coords[1]] : [relativeY], base: command.base, coords: command.coords, }); - else if (command.command == 'A') { - const data = - /** @type {{rx: number, ry: number, r: number, large: boolean, sweep: boolean}} */ ( - command.arc - ); - const args = [ - data.rx, - data.ry, - data.r, - data.large ? 1 : 0, - data.sweep ? 1 : 0, - ]; - const relativeX = command.coords[0] - command.base[0]; - const relativeY = command.coords[1] - command.base[1]; + } else { const absoluteLength = - estimateLength([...args, ...command.coords], precision) + - (lastCommand == 'A' ? 0 : 1); + estimateLength(command.coords, precision) + + (lastCommand == 'L' ? 0 : 1); const relativeLength = - estimateLength([...args, relativeX, relativeY], precision) + - (lastCommand == 'a' ? 0 : 1); + estimateLength([relativeX, relativeY], precision) + + (lastCommand == 'l' ? 0 : 1); acc.push({ - command: absoluteLength < relativeLength ? 'A' : 'a', + command: absoluteLength < relativeLength ? 'L' : 'l', args: absoluteLength < relativeLength - ? [...args, ...command.coords] - : [...args, relativeX, relativeY], + ? command.coords + : [relativeX, relativeY], base: command.base, coords: command.coords, }); - } else if (command.command == 'L') { - const relativeX = command.coords[0] - command.base[0]; - const relativeY = command.coords[1] - command.base[1]; - if (acc.length == path.length - 1 && canUseZ) { - acc.push({ - command: 'z', - args: [], - base: command.base, - coords: command.coords, - }); - } else if (command.base[1] == command.coords[1]) { - const absoluteLength = toPrecision( - command.coords[0], - precision - ).toString().length; - const relativeLength = toPrecision(relativeX, precision).toString() - .length; - acc.push({ - command: absoluteLength < relativeLength ? 'H' : 'h', - args: - absoluteLength < relativeLength ? [command.coords[0]] : [relativeX], - base: command.base, - coords: command.coords, - }); - } else if (command.base[0] == command.coords[0]) { - const absoluteLength = toPrecision( - command.coords[1], - precision - ).toString().length; - const relativeLength = toPrecision(relativeY, precision).toString() - .length; - acc.push({ - command: absoluteLength < relativeLength ? 'V' : 'v', - args: - absoluteLength < relativeLength ? [command.coords[1]] : [relativeY], - base: command.base, - coords: command.coords, - }); - } else { - const absoluteLength = - estimateLength(command.coords, precision) + - (lastCommand == 'L' ? 0 : 1); - const relativeLength = - estimateLength([relativeX, relativeY], precision) + - (lastCommand == 'l' ? 0 : 1); - acc.push({ - command: absoluteLength < relativeLength ? 'L' : 'l', - args: - absoluteLength < relativeLength - ? command.coords - : [relativeX, relativeY], - base: command.base, - coords: command.coords, - }); } - } - } - return acc; + } + } + return acc; } /** @@ -380,8 +380,5 @@ function toPrecision(number, precision) { * @param {number} precision */ function estimateLength(numbers, precision) { - return cleanupOutData( - numbers.map((n) => toPrecision(n, precision)), - {} - ).length; + return stringifyArgs('L', numbers, precision).length; } From 2016d6a7fb6e270b22cb0048c444774c0aa698bc Mon Sep 17 00:00:00 2001 From: Kendell R Date: Fri, 24 Nov 2023 07:31:10 -0800 Subject: [PATCH 11/32] properly handle fill none --- plugins/optimizePathOrder.js | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/plugins/optimizePathOrder.js b/plugins/optimizePathOrder.js index 8ecd6cfa0..b42c7456f 100644 --- a/plugins/optimizePathOrder.js +++ b/plugins/optimizePathOrder.js @@ -45,16 +45,20 @@ exports.fn = (root, params) => { const maybeHasStroke = computedStyle.stroke && - (computedStyle.stroke.type === 'dynamic' || - computedStyle.stroke.value !== 'none'); + (computedStyle.stroke.type == 'dynamic' || + computedStyle.stroke.value != 'none'); + const maybeHasFill = + computedStyle.fill && + (computedStyle.fill.type == 'dynamic' || + computedStyle.fill.value != 'none'); const unsafeToChangeStart = maybeHasStroke - ? computedStyle['stroke-linecap']?.type !== 'static' || - computedStyle['stroke-linecap'].value !== 'round' || - computedStyle['stroke-linejoin']?.type !== 'static' || - computedStyle['stroke-linejoin'].value !== 'round' + ? computedStyle['stroke-linecap']?.type != 'static' || + computedStyle['stroke-linecap'].value != 'round' || + computedStyle['stroke-linejoin']?.type != 'static' || + computedStyle['stroke-linejoin'].value != 'round' : false; - const unsafeToChangeDirection = computedStyle.fill - ? computedStyle['fill-rule']?.type !== 'static' || + const unsafeToChangeDirection = maybeHasFill + ? computedStyle['fill-rule']?.type != 'static' || computedStyle['fill-rule'].value == 'nonzero' : false; From d7bf163dd1f78f5b49b017f4f6133db439cde20e Mon Sep 17 00:00:00 2001 From: Kendell R Date: Fri, 24 Nov 2023 08:21:39 -0800 Subject: [PATCH 12/32] use an even faster way of stringifying args --- lib/path.js | 1 - plugins/optimizePathOrder.js | 21 +++++++++++++++++++-- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/lib/path.js b/lib/path.js index d959c7c5b..c8782ce80 100644 --- a/lib/path.js +++ b/lib/path.js @@ -346,5 +346,4 @@ const stringifyPathData = ({ pathData, precision, disableSpaceAfterFlags }) => { } return result; }; -exports.stringifyArgs = stringifyArgs; exports.stringifyPathData = stringifyPathData; diff --git a/plugins/optimizePathOrder.js b/plugins/optimizePathOrder.js index b42c7456f..b4a02885d 100644 --- a/plugins/optimizePathOrder.js +++ b/plugins/optimizePathOrder.js @@ -1,4 +1,4 @@ -const { stringifyPathData, stringifyArgs } = require('../lib/path.js'); +const { stringifyPathData } = require('../lib/path.js'); const { computeStyle, collectStylesheet } = require('../lib/style.js'); const { hasScripts } = require('../lib/svgo/tools.js'); const { pathElems } = require('./_collections.js'); @@ -384,5 +384,22 @@ function toPrecision(number, precision) { * @param {number} precision */ function estimateLength(numbers, precision) { - return stringifyArgs('L', numbers, precision).length; + let length = 0; + let last = undefined; + for (const number of numbers) { + const rounded = toPrecision(number, precision); + const string = rounded.toString(); + length += + string.length - (rounded != 0 && rounded > -1 && rounded < 1 ? 1 : 0); + if (last) { + if ( + !(rounded < 0) && + !(last.includes('.') && rounded > 0 && rounded < 1) + ) { + length += 1; + } + } + last = string; + } + return length; } From ad9d5da28e64dcbe5736bcab1751791ef536cf0f Mon Sep 17 00:00:00 2001 From: Kendell R Date: Fri, 24 Nov 2023 09:03:24 -0800 Subject: [PATCH 13/32] faster way to estimate path length --- plugins/optimizePathOrder.js | 46 ++++++++++++++++++++++++++++++++---- 1 file changed, 42 insertions(+), 4 deletions(-) diff --git a/plugins/optimizePathOrder.js b/plugins/optimizePathOrder.js index b4a02885d..fd4c51e2b 100644 --- a/plugins/optimizePathOrder.js +++ b/plugins/optimizePathOrder.js @@ -130,6 +130,7 @@ exports.fn = (root, params) => { start[0] != end[0] || start[1] != end[1], unsafeToChangeDirection, + first: i == 0, next: next?.data[0].command == 'm' ? next.data[0] : undefined, baseline: part.data, precision, @@ -158,6 +159,7 @@ exports.fn = (root, params) => { * path: InternalPath[], * unsafeToChangeStart: boolean, * unsafeToChangeDirection: boolean, + * first: boolean, * next: RealPath | undefined, * baseline: RealPath[], * precision: number @@ -167,6 +169,7 @@ function optimizePart({ path, unsafeToChangeStart, unsafeToChangeDirection, + first, next, baseline, precision, @@ -229,10 +232,7 @@ function optimizePart({ const outputPath = transformPath(output, precision, !unsafeToChangeStart); const size = - stringifyPathData({ - pathData: outputPath, - precision, - }).length + + estimatePathLength(outputPath, precision, first) + (next ? estimateLength( transformMove(next, output[output.length - 1].coords), @@ -403,3 +403,41 @@ function estimateLength(numbers, precision) { } return length; } + +/** + * @param {PathDataItem[]} data + * @param {number} precision + * @param {boolean} first + */ +function estimatePathLength(data, precision, first) { + /** + * @type {{command: string, args: number[]}[]} + */ + let combined = []; + for (const command of data) { + const last = combined[combined.length - 1]; + if (last) { + const commandless = + (last.command == command.command && + last.command != 'M' && + last.command != 'm') || + (last.command == 'M' && command.command == 'L') || + (last.command == 'm' && command.command == 'l') || + (first && + combined.length == 1 && + (command.command == 'L' || command.command == 'l')); + if (commandless) { + last.args = [...last.args, ...command.args]; + continue; + } + } + combined.push({ command: command.command, args: command.args }); + } + + let length = 0; + for (const command of combined) { + length += 1 + estimateLength(command.args, precision); + } + + return length; +} From 876afcbf7fee361830068d30298aa71acc8ae90c Mon Sep 17 00:00:00 2001 From: Kendell R Date: Fri, 24 Nov 2023 09:22:41 -0800 Subject: [PATCH 14/32] don't run process on identical version --- plugins/optimizePathOrder.js | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/optimizePathOrder.js b/plugins/optimizePathOrder.js index fd4c51e2b..3234487d0 100644 --- a/plugins/optimizePathOrder.js +++ b/plugins/optimizePathOrder.js @@ -186,6 +186,7 @@ function optimizePart({ }; for (const start of starts) { for (const reverse of unsafeToChangeDirection ? [false] : [false, true]) { + if (start == 0 && !reverse) continue; const data = reverse ? path .slice(0, start) From 94f45cd6d1771aaee2bc3a7cc99171a24b195360 Mon Sep 17 00:00:00 2001 From: Kendell R Date: Fri, 24 Nov 2023 12:10:04 -0800 Subject: [PATCH 15/32] use estimation in base size, fix estimation --- plugins/optimizePathOrder.js | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/plugins/optimizePathOrder.js b/plugins/optimizePathOrder.js index 3234487d0..014f4e793 100644 --- a/plugins/optimizePathOrder.js +++ b/plugins/optimizePathOrder.js @@ -1,4 +1,3 @@ -const { stringifyPathData } = require('../lib/path.js'); const { computeStyle, collectStylesheet } = require('../lib/style.js'); const { hasScripts } = require('../lib/svgo/tools.js'); const { pathElems } = require('./_collections.js'); @@ -180,7 +179,7 @@ function optimizePart({ let best = { success: false, size: - stringifyPathData({ pathData: baseline, precision }).length + + estimatePathLength(baseline, precision, first) + (next ? estimateLength(next.args, precision) : 0), data: baseline, }; @@ -415,25 +414,31 @@ function estimatePathLength(data, precision, first) { * @type {{command: string, args: number[]}[]} */ let combined = []; - for (const command of data) { + data.forEach((command, i) => { const last = combined[combined.length - 1]; if (last) { - const commandless = + let commandless = (last.command == command.command && last.command != 'M' && last.command != 'm') || (last.command == 'M' && command.command == 'L') || - (last.command == 'm' && command.command == 'l') || - (first && - combined.length == 1 && - (command.command == 'L' || command.command == 'l')); + (last.command == 'm' && command.command == 'l'); + if ( + first && + i == 1 && + (last.command == 'M' || last.command == 'm') && + (command.command == 'L' || command.command == 'l') + ) { + commandless = true; + last.command = command.command == 'L' ? 'M' : 'm'; + } if (commandless) { last.args = [...last.args, ...command.args]; - continue; + return; } } combined.push({ command: command.command, args: command.args }); - } + }); let length = 0; for (const command of combined) { From 87bb7360770b53db3675707ffffb98e35795c508 Mon Sep 17 00:00:00 2001 From: Kendell R Date: Fri, 24 Nov 2023 13:49:46 -0800 Subject: [PATCH 16/32] also optimize curves --- plugins/optimizePathOrder.js | 94 ++++++++++++++++++++++++++++++++++-- 1 file changed, 91 insertions(+), 3 deletions(-) diff --git a/plugins/optimizePathOrder.js b/plugins/optimizePathOrder.js index 014f4e793..8d751f739 100644 --- a/plugins/optimizePathOrder.js +++ b/plugins/optimizePathOrder.js @@ -9,7 +9,7 @@ exports.description = 'Moves around instructions in paths to be optimal.'; /** * @typedef {import('../lib/types').PathDataCommand} PathDataCommand * @typedef {import('../lib/types').PathDataItem} PathDataItem - * @typedef {{command: PathDataCommand, arc?: {rx: number, ry: number, r: number, large: boolean, sweep: boolean}, base: [number, number], coords: [number, number]}} InternalPath + * @typedef {{command: PathDataCommand, arc?: {rx: number, ry: number, r: number, large: boolean, sweep: boolean}, c?: {x1: number, y1: number, x2: number, y2: number}, q?: {x: number, y: number}, base: [number, number], coords: [number, number]}} InternalPath * @typedef {PathDataItem & {base: [number, number], coords: [number, number]}} RealPath */ @@ -83,6 +83,10 @@ exports.fn = (root, params) => { instruction.command != 'V' && instruction.command != 'a' && instruction.command != 'A' && + instruction.command != 'C' && + instruction.command != 'c' && + instruction.command != 'Q' && + instruction.command != 'q' && instruction.command != 'z' && instruction.command != 'Z' ) { @@ -121,6 +125,34 @@ exports.fn = (root, params) => { sweep: Boolean(item.args[4]), } : undefined, + c: + item.command == 'c' + ? { + x1: item.args[0] + item.base[0], + y1: item.args[1] + item.base[1], + x2: item.args[2] + item.base[0], + y2: item.args[3] + item.base[1], + } + : item.command == 'C' + ? { + x1: item.args[0], + y1: item.args[1], + x2: item.args[2], + y2: item.args[3], + } + : undefined, + q: + item.command == 'q' + ? { + x: item.args[0] + item.base[0], + y: item.args[1] + item.base[1], + } + : item.command == 'Q' + ? { + x: item.args[0], + y: item.args[1], + } + : undefined, base: item.base, coords: item.coords, })), @@ -198,6 +230,13 @@ function optimizePart({ ...item.arc, sweep: !item.arc.sweep, }, + c: item.c && { + x1: item.c.x2, + y1: item.c.y2, + x2: item.c.x1, + y2: item.c.y1, + }, + q: item.q, base: item.coords, coords: item.base, }; @@ -221,6 +260,20 @@ function optimizePart({ base: item.base, coords: item.coords, }); + } else if (item.command == 'c' || item.command == 'C') { + output.push({ + command: 'C', + c: item.c, + base: item.base, + coords: item.coords, + }); + } else if (item.command == 'q' || item.command == 'Q') { + output.push({ + command: 'Q', + q: item.q, + base: item.base, + coords: item.coords, + }); } else { output.push({ command: 'L', @@ -266,14 +319,14 @@ function transformPath(path, precision, canUseZ) { for (const command of path) { const lastCommand = acc[acc.length - 1]?.command; - if (command.command == 'M') + if (command.command == 'M') { acc.push({ command: 'M', args: command.coords, base: command.base, coords: command.coords, }); - else if (command.command == 'A') { + } else if (command.command == 'A') { const data = /** @type {{rx: number, ry: number, r: number, large: boolean, sweep: boolean}} */ ( command.arc @@ -302,6 +355,41 @@ function transformPath(path, precision, canUseZ) { base: command.base, coords: command.coords, }); + } else if (command.command == 'C') { + const data = + /** @type {{x1: number, y1: number, x2: number, y2: number}} */ ( + command.c + ); + const args = [data.x1, data.y1, data.x2, data.y2, ...command.coords]; + const argsRelative = args.map((a, i) => + i % 2 == 0 ? a - command.base[0] : a - command.base[1] + ); + const absoluteLength = + estimateLength(args, precision) + (lastCommand == 'C' ? 0 : 1); + const relativeLength = + estimateLength(argsRelative, precision) + (lastCommand == 'c' ? 0 : 1); + acc.push({ + command: absoluteLength < relativeLength ? 'C' : 'c', + args: absoluteLength < relativeLength ? args : argsRelative, + base: command.base, + coords: command.coords, + }); + } else if (command.command == 'Q') { + const data = /** @type {{x: number, y: number}} */ (command.q); + const args = [data.x, data.y, ...command.coords]; + const argsRelative = args.map((a, i) => + i % 2 == 0 ? a - command.base[0] : a - command.base[1] + ); + const absoluteLength = + estimateLength(args, precision) + (lastCommand == 'Q' ? 0 : 1); + const relativeLength = + estimateLength(argsRelative, precision) + (lastCommand == 'q' ? 0 : 1); + acc.push({ + command: absoluteLength < relativeLength ? 'Q' : 'q', + args: absoluteLength < relativeLength ? args : argsRelative, + base: command.base, + coords: command.coords, + }); } else if (command.command == 'L') { const relativeX = command.coords[0] - command.base[0]; const relativeY = command.coords[1] - command.base[1]; From a2bf7ec50855701f540ff9a7f63c5edad7b6e4b2 Mon Sep 17 00:00:00 2001 From: Kendell R Date: Fri, 24 Nov 2023 14:01:21 -0800 Subject: [PATCH 17/32] add polylineOnly --- docs/03-plugins/optimize-path-order.mdx | 11 +++++++---- plugins/optimizePathOrder.js | 22 ++++++++++++++-------- plugins/plugins-types.ts | 1 + 3 files changed, 22 insertions(+), 12 deletions(-) diff --git a/docs/03-plugins/optimize-path-order.mdx b/docs/03-plugins/optimize-path-order.mdx index d70b1d63b..8dd9a3269 100644 --- a/docs/03-plugins/optimize-path-order.mdx +++ b/docs/03-plugins/optimize-path-order.mdx @@ -10,22 +10,25 @@ svgo: noSpaceAfterFlags: description: If to omit spaces after flags. Flags are values that can only be 0 or 1 and are used by some path commands, namely A and a. default: false + polylineOnly: + description: If to skip path parts that include curves. This will speed up optimization but not get as good of a result. + default: false --- Optimizes parts of paths by starting in different places and reversing them. ## Usage - + ### Parameters - + ## Demo - + ## Implementation -* https://github.com/svg/svgo/blob/main/plugins/optimizePathOrder.js +- https://github.com/svg/svgo/blob/main/plugins/optimizePathOrder.js diff --git a/plugins/optimizePathOrder.js b/plugins/optimizePathOrder.js index 8d751f739..0261c423b 100644 --- a/plugins/optimizePathOrder.js +++ b/plugins/optimizePathOrder.js @@ -21,7 +21,11 @@ exports.description = 'Moves around instructions in paths to be optimal.'; * @type {import('./plugins-types').Plugin<'optimizePathOrder'>} */ exports.fn = (root, params) => { - const { floatPrecision: precision = 3, noSpaceAfterFlags } = params; + const { + floatPrecision: precision = 3, + noSpaceAfterFlags = false, + polylineOnly = false, + } = params; const stylesheet = collectStylesheet(root); let deoptimized = false; return { @@ -81,14 +85,16 @@ exports.fn = (root, params) => { instruction.command != 'H' && instruction.command != 'v' && instruction.command != 'V' && - instruction.command != 'a' && - instruction.command != 'A' && - instruction.command != 'C' && - instruction.command != 'c' && - instruction.command != 'Q' && - instruction.command != 'q' && instruction.command != 'z' && - instruction.command != 'Z' + instruction.command != 'Z' && + (polylineOnly + ? true + : instruction.command != 'a' && + instruction.command != 'A' && + instruction.command != 'C' && + instruction.command != 'c' && + instruction.command != 'Q' && + instruction.command != 'q') ) { part.valid = false; } diff --git a/plugins/plugins-types.ts b/plugins/plugins-types.ts index d43ab2d17..fcb2b62de 100644 --- a/plugins/plugins-types.ts +++ b/plugins/plugins-types.ts @@ -141,6 +141,7 @@ type DefaultPlugins = { optimizePathOrder: { floatPrecision?: number; noSpaceAfterFlags?: boolean; + polylineOnly?: boolean; }; removeComments: { preservePatterns: Array | false; From b2ea7a2715a6cef8891ce619bdfa020ec1236ddc Mon Sep 17 00:00:00 2001 From: Kendell R Date: Fri, 24 Nov 2023 18:18:47 -0800 Subject: [PATCH 18/32] go from transformPath to transformCommand --- plugins/optimizePathOrder.js | 293 ++++++++++++++++------------------- 1 file changed, 133 insertions(+), 160 deletions(-) diff --git a/plugins/optimizePathOrder.js b/plugins/optimizePathOrder.js index 0261c423b..3f05a2e6b 100644 --- a/plugins/optimizePathOrder.js +++ b/plugins/optimizePathOrder.js @@ -250,48 +250,36 @@ function optimizePart({ : path.slice(start).concat(path.slice(0, start)); /** - * @type {InternalPath[]} + * @type {RealPath[]} */ const output = []; output.push({ command: 'M', + args: data[0].base, base: [0, 0], - coords: [data[0].base[0], data[0].base[1]], + coords: data[0].base, }); for (const item of data) { - if (item.command == 'a' || item.command == 'A') { - output.push({ - command: 'A', - arc: item.arc, - base: item.base, - coords: item.coords, - }); - } else if (item.command == 'c' || item.command == 'C') { - output.push({ - command: 'C', - c: item.c, - base: item.base, - coords: item.coords, - }); - } else if (item.command == 'q' || item.command == 'Q') { - output.push({ - command: 'Q', - q: item.q, - base: item.base, - coords: item.coords, - }); - } else { - output.push({ - command: 'L', - base: item.base, - coords: item.coords, - }); - } + const command = + item.command == 'a' || item.command == 'A' + ? 'A' + : item.command == 'c' || item.command == 'C' + ? 'C' + : item.command == 'q' || item.command == 'Q' + ? 'Q' + : 'L'; + output.push( + transformCommand( + { ...item, command: command }, + precision, + output[output.length - 1].command, + output.length - 1 == data.length - 1 && !unsafeToChangeStart + ) + ); } - const outputPath = transformPath(output, precision, !unsafeToChangeStart); const size = - estimatePathLength(outputPath, precision, first) + + estimatePathLength(output, precision, first) + (next ? estimateLength( transformMove(next, output[output.length - 1].coords), @@ -304,7 +292,7 @@ function optimizePart({ best = { success: true, size, - data: outputPath, + data: output, }; } } @@ -313,149 +301,134 @@ function optimizePart({ } /** - * @param {InternalPath[]} path + * @param {InternalPath & {command: "A" | "C" | "Q" | "L"}} command * @param {number} precision - * @param {boolean} canUseZ + * @param {string} lastCommand + * @param {boolean} useZ + * @returns {RealPath} */ -function transformPath(path, precision, canUseZ) { - /** - * @type {RealPath[]} - */ - const acc = []; - for (const command of path) { - const lastCommand = acc[acc.length - 1]?.command; - - if (command.command == 'M') { - acc.push({ - command: 'M', - args: command.coords, +function transformCommand(command, precision, lastCommand, useZ) { + if (command.command == 'A') { + const data = + /** @type {{rx: number, ry: number, r: number, large: boolean, sweep: boolean}} */ ( + command.arc + ); + const args = [ + data.rx, + data.ry, + data.r, + data.large ? 1 : 0, + data.sweep ? 1 : 0, + ]; + const relativeX = command.coords[0] - command.base[0]; + const relativeY = command.coords[1] - command.base[1]; + const absoluteLength = + estimateLength([...args, ...command.coords], precision) + + (lastCommand == 'A' ? 0 : 1); + const relativeLength = + estimateLength([...args, relativeX, relativeY], precision) + + (lastCommand == 'a' ? 0 : 1); + return { + command: absoluteLength < relativeLength ? 'A' : 'a', + args: + absoluteLength < relativeLength + ? [...args, ...command.coords] + : [...args, relativeX, relativeY], + base: command.base, + coords: command.coords, + }; + } else if (command.command == 'C') { + const data = + /** @type {{x1: number, y1: number, x2: number, y2: number}} */ ( + command.c + ); + const args = [data.x1, data.y1, data.x2, data.y2, ...command.coords]; + const argsRelative = args.map((a, i) => + i % 2 == 0 ? a - command.base[0] : a - command.base[1] + ); + const absoluteLength = + estimateLength(args, precision) + (lastCommand == 'C' ? 0 : 1); + const relativeLength = + estimateLength(argsRelative, precision) + (lastCommand == 'c' ? 0 : 1); + return { + command: absoluteLength < relativeLength ? 'C' : 'c', + args: absoluteLength < relativeLength ? args : argsRelative, + base: command.base, + coords: command.coords, + }; + } else if (command.command == 'Q') { + const data = /** @type {{x: number, y: number}} */ (command.q); + const args = [data.x, data.y, ...command.coords]; + const argsRelative = args.map((a, i) => + i % 2 == 0 ? a - command.base[0] : a - command.base[1] + ); + const absoluteLength = + estimateLength(args, precision) + (lastCommand == 'Q' ? 0 : 1); + const relativeLength = + estimateLength(argsRelative, precision) + (lastCommand == 'q' ? 0 : 1); + return { + command: absoluteLength < relativeLength ? 'Q' : 'q', + args: absoluteLength < relativeLength ? args : argsRelative, + base: command.base, + coords: command.coords, + }; + } else { + const relativeX = command.coords[0] - command.base[0]; + const relativeY = command.coords[1] - command.base[1]; + if (useZ) { + return { + command: 'z', + args: [], base: command.base, coords: command.coords, - }); - } else if (command.command == 'A') { - const data = - /** @type {{rx: number, ry: number, r: number, large: boolean, sweep: boolean}} */ ( - command.arc - ); - const args = [ - data.rx, - data.ry, - data.r, - data.large ? 1 : 0, - data.sweep ? 1 : 0, - ]; - const relativeX = command.coords[0] - command.base[0]; - const relativeY = command.coords[1] - command.base[1]; - const absoluteLength = - estimateLength([...args, ...command.coords], precision) + - (lastCommand == 'A' ? 0 : 1); - const relativeLength = - estimateLength([...args, relativeX, relativeY], precision) + - (lastCommand == 'a' ? 0 : 1); - acc.push({ - command: absoluteLength < relativeLength ? 'A' : 'a', + }; + } else if (command.base[1] == command.coords[1]) { + const absoluteLength = toPrecision( + command.coords[0], + precision + ).toString().length; + const relativeLength = toPrecision(relativeX, precision).toString() + .length; + return { + command: absoluteLength < relativeLength ? 'H' : 'h', args: - absoluteLength < relativeLength - ? [...args, ...command.coords] - : [...args, relativeX, relativeY], + absoluteLength < relativeLength ? [command.coords[0]] : [relativeX], base: command.base, coords: command.coords, - }); - } else if (command.command == 'C') { - const data = - /** @type {{x1: number, y1: number, x2: number, y2: number}} */ ( - command.c - ); - const args = [data.x1, data.y1, data.x2, data.y2, ...command.coords]; - const argsRelative = args.map((a, i) => - i % 2 == 0 ? a - command.base[0] : a - command.base[1] - ); - const absoluteLength = - estimateLength(args, precision) + (lastCommand == 'C' ? 0 : 1); - const relativeLength = - estimateLength(argsRelative, precision) + (lastCommand == 'c' ? 0 : 1); - acc.push({ - command: absoluteLength < relativeLength ? 'C' : 'c', - args: absoluteLength < relativeLength ? args : argsRelative, + }; + } else if (command.base[0] == command.coords[0]) { + const absoluteLength = toPrecision( + command.coords[1], + precision + ).toString().length; + const relativeLength = toPrecision(relativeY, precision).toString() + .length; + return { + command: absoluteLength < relativeLength ? 'V' : 'v', + args: + absoluteLength < relativeLength ? [command.coords[1]] : [relativeY], base: command.base, coords: command.coords, - }); - } else if (command.command == 'Q') { - const data = /** @type {{x: number, y: number}} */ (command.q); - const args = [data.x, data.y, ...command.coords]; - const argsRelative = args.map((a, i) => - i % 2 == 0 ? a - command.base[0] : a - command.base[1] - ); + }; + } else { const absoluteLength = - estimateLength(args, precision) + (lastCommand == 'Q' ? 0 : 1); + estimateLength(command.coords, precision) + + (lastCommand == 'L' ? 0 : 1); const relativeLength = - estimateLength(argsRelative, precision) + (lastCommand == 'q' ? 0 : 1); - acc.push({ - command: absoluteLength < relativeLength ? 'Q' : 'q', - args: absoluteLength < relativeLength ? args : argsRelative, + estimateLength([relativeX, relativeY], precision) + + (lastCommand == 'l' ? 0 : 1); + return { + command: absoluteLength < relativeLength ? 'L' : 'l', + args: + absoluteLength < relativeLength + ? command.coords + : [relativeX, relativeY], base: command.base, coords: command.coords, - }); - } else if (command.command == 'L') { - const relativeX = command.coords[0] - command.base[0]; - const relativeY = command.coords[1] - command.base[1]; - if (acc.length == path.length - 1 && canUseZ) { - acc.push({ - command: 'z', - args: [], - base: command.base, - coords: command.coords, - }); - } else if (command.base[1] == command.coords[1]) { - const absoluteLength = toPrecision( - command.coords[0], - precision - ).toString().length; - const relativeLength = toPrecision(relativeX, precision).toString() - .length; - acc.push({ - command: absoluteLength < relativeLength ? 'H' : 'h', - args: - absoluteLength < relativeLength ? [command.coords[0]] : [relativeX], - base: command.base, - coords: command.coords, - }); - } else if (command.base[0] == command.coords[0]) { - const absoluteLength = toPrecision( - command.coords[1], - precision - ).toString().length; - const relativeLength = toPrecision(relativeY, precision).toString() - .length; - acc.push({ - command: absoluteLength < relativeLength ? 'V' : 'v', - args: - absoluteLength < relativeLength ? [command.coords[1]] : [relativeY], - base: command.base, - coords: command.coords, - }); - } else { - const absoluteLength = - estimateLength(command.coords, precision) + - (lastCommand == 'L' ? 0 : 1); - const relativeLength = - estimateLength([relativeX, relativeY], precision) + - (lastCommand == 'l' ? 0 : 1); - acc.push({ - command: absoluteLength < relativeLength ? 'L' : 'l', - args: - absoluteLength < relativeLength - ? command.coords - : [relativeX, relativeY], - base: command.base, - coords: command.coords, - }); - } + }; } } - return acc; } - /** * @param {RealPath} command * @param {[number, number]} newBase From b54e168019b73dd33ebb978f0fea782ec729e9ad Mon Sep 17 00:00:00 2001 From: Kendell R Date: Sat, 25 Nov 2023 06:59:21 -0800 Subject: [PATCH 19/32] speed: roll through commands --- plugins/optimizePathOrder.js | 136 +++++++++++++++++++++-------------- 1 file changed, 84 insertions(+), 52 deletions(-) diff --git a/plugins/optimizePathOrder.js b/plugins/optimizePathOrder.js index 3f05a2e6b..73ee8d197 100644 --- a/plugins/optimizePathOrder.js +++ b/plugins/optimizePathOrder.js @@ -211,9 +211,6 @@ function optimizePart({ baseline, precision, }) { - const starts = unsafeToChangeStart - ? [0] - : Array.from({ length: path.length }, (_, i) => i); let best = { success: false, size: @@ -221,62 +218,83 @@ function optimizePart({ (next ? estimateLength(next.args, precision) : 0), data: baseline, }; - for (const start of starts) { - for (const reverse of unsafeToChangeDirection ? [false] : [false, true]) { - if (start == 0 && !reverse) continue; - const data = reverse - ? path - .slice(0, start) - .reverse() - .concat(path.slice(start).reverse()) - .map((item) => { - return { - command: item.command, - arc: item.arc && { - ...item.arc, - sweep: !item.arc.sweep, - }, - c: item.c && { - x1: item.c.x2, - y1: item.c.y2, - x2: item.c.x1, - y2: item.c.y1, - }, - q: item.q, - base: item.coords, - coords: item.base, - }; - }) - : path.slice(start).concat(path.slice(0, start)); + for (const reverse of unsafeToChangeDirection ? [false] : [false, true]) { + const input = reverse + ? path + .map((item) => { + return { + command: item.command, + arc: item.arc && { + ...item.arc, + sweep: !item.arc.sweep, + }, + c: item.c && { + x1: item.c.x2, + y1: item.c.y2, + x2: item.c.x1, + y2: item.c.y1, + }, + q: item.q, + base: item.coords, + coords: item.base, + }; + }) + .reverse() + : path; + /** + * @type {RealPath[]} + */ + const output = []; + let i = 0; + while (unsafeToChangeStart ? i == 0 : i < path.length) { + if (i == 0) { + output.push({ + command: 'M', + args: input[0].base, + base: [0, 0], + coords: input[0].base, + }); + for (const item of input) { + output.push( + transformCommand( + { ...item, command: getCommand(item) }, + precision, + output[output.length - 1].command, + output.length == input.length && !unsafeToChangeStart + ) + ); + } + } else { + const previousItem = i == 1 ? input[input.length - 1] : input[i - 2]; + const newItem = input[i - 1]; + // Cycle start out and into end + output.splice(1, 1); + output[0] = { + command: 'M', + args: output[1].base, + base: [0, 0], + coords: output[1].base, + }; - /** - * @type {RealPath[]} - */ - const output = []; - output.push({ - command: 'M', - args: data[0].base, - base: [0, 0], - coords: data[0].base, - }); - for (const item of data) { - const command = - item.command == 'a' || item.command == 'A' - ? 'A' - : item.command == 'c' || item.command == 'C' - ? 'C' - : item.command == 'q' || item.command == 'Q' - ? 'Q' - : 'L'; + output.pop(); output.push( transformCommand( - { ...item, command: command }, + { ...previousItem, command: getCommand(previousItem) }, precision, output[output.length - 1].command, - output.length - 1 == data.length - 1 && !unsafeToChangeStart + false + ) + ); + output.push( + transformCommand( + { ...newItem, command: getCommand(newItem) }, + precision, + output[output.length - 1].command, + !unsafeToChangeStart ) ); } + i++; const size = estimatePathLength(output, precision, first) + @@ -292,7 +310,7 @@ function optimizePart({ best = { success: true, size, - data: output, + data: [...output], }; } } @@ -300,6 +318,19 @@ function optimizePart({ return best; } +/** + * @param {InternalPath} item + */ +function getCommand(item) { + return item.command == 'a' || item.command == 'A' + ? 'A' + : item.command == 'c' || item.command == 'C' + ? 'C' + : item.command == 'q' || item.command == 'Q' + ? 'Q' + : 'L'; +} + /** * @param {InternalPath & {command: "A" | "C" | "Q" | "L"}} command * @param {number} precision @@ -429,6 +460,7 @@ function transformCommand(command, precision, lastCommand, useZ) { } } } + /** * @param {RealPath} command * @param {[number, number]} newBase From 25377cd397a6ad290444e9f14a9bc240f0d05a2d Mon Sep 17 00:00:00 2001 From: Kendell R Date: Sat, 25 Nov 2023 07:31:56 -0800 Subject: [PATCH 20/32] also stage the shortened svgs --- test/coa/testSvg/test.1.svg | 2 +- test/coa/testSvg/test.svg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/coa/testSvg/test.1.svg b/test/coa/testSvg/test.1.svg index bd829c7e5..83dce264c 100644 --- a/test/coa/testSvg/test.1.svg +++ b/test/coa/testSvg/test.1.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/test/coa/testSvg/test.svg b/test/coa/testSvg/test.svg index bd829c7e5..83dce264c 100644 --- a/test/coa/testSvg/test.svg +++ b/test/coa/testSvg/test.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file From 688361206e578ff888a77e9079c061874dc594cb Mon Sep 17 00:00:00 2001 From: Kendell R Date: Sat, 25 Nov 2023 12:21:27 -0800 Subject: [PATCH 21/32] assume fill --- plugins/optimizePathOrder.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/optimizePathOrder.js b/plugins/optimizePathOrder.js index 73ee8d197..25202e7a1 100644 --- a/plugins/optimizePathOrder.js +++ b/plugins/optimizePathOrder.js @@ -51,9 +51,9 @@ exports.fn = (root, params) => { (computedStyle.stroke.type == 'dynamic' || computedStyle.stroke.value != 'none'); const maybeHasFill = - computedStyle.fill && - (computedStyle.fill.type == 'dynamic' || - computedStyle.fill.value != 'none'); + !computedStyle.fill || + computedStyle.fill.type == 'dynamic' || + computedStyle.fill.value != 'none'; const unsafeToChangeStart = maybeHasStroke ? computedStyle['stroke-linecap']?.type != 'static' || computedStyle['stroke-linecap'].value != 'round' || From ab5a0984f4253d2ab98b1908d94a324fa5e4c16c Mon Sep 17 00:00:00 2001 From: Kendell R Date: Sun, 26 Nov 2023 08:47:25 -0800 Subject: [PATCH 22/32] allow changing direction if there is only one part --- plugins/optimizePathOrder.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/optimizePathOrder.js b/plugins/optimizePathOrder.js index 25202e7a1..a334cb0f1 100644 --- a/plugins/optimizePathOrder.js +++ b/plugins/optimizePathOrder.js @@ -166,7 +166,8 @@ exports.fn = (root, params) => { unsafeToChangeStart || start[0] != end[0] || start[1] != end[1], - unsafeToChangeDirection, + unsafeToChangeDirection: + parts.length < 2 ? false : unsafeToChangeDirection, first: i == 0, next: next?.data[0].command == 'm' ? next.data[0] : undefined, baseline: part.data, From c0c4cf04cf82d0a0d9916e81a5f276ad5e12a2be Mon Sep 17 00:00:00 2001 From: Kendell R Date: Sun, 3 Dec 2023 11:53:32 -0800 Subject: [PATCH 23/32] don't crash if convertPathData is off and add tests --- plugins/optimizePathOrder.js | 8 ++++++++ test/plugins/optimizePathOrder.test.js | 20 ++++++++++++++++++++ 2 files changed, 28 insertions(+) create mode 100644 test/plugins/optimizePathOrder.test.js diff --git a/plugins/optimizePathOrder.js b/plugins/optimizePathOrder.js index a334cb0f1..f954dd42b 100644 --- a/plugins/optimizePathOrder.js +++ b/plugins/optimizePathOrder.js @@ -66,6 +66,14 @@ exports.fn = (root, params) => { : false; const path = /** @type {RealPath[]} */ (path2js(node)); + if (path[0] && !path[0].coords) { + console.warn( + 'optimizePathOrder is enabled, but data from convertPathData is not present. ' + + 'If you want to use optimizePathOrder, enable convertPathData and put optimizePathOrder after it. ' + + 'If you do not want to use optimizePathOrder, disable it.' + ); + return; + } const parts = []; let part = { valid: true, data: /** @type {RealPath[]} */ ([]) }; diff --git a/test/plugins/optimizePathOrder.test.js b/test/plugins/optimizePathOrder.test.js new file mode 100644 index 000000000..3e2bb44ff --- /dev/null +++ b/test/plugins/optimizePathOrder.test.js @@ -0,0 +1,20 @@ +'use strict'; + +const { optimize } = require('../../lib/svgo.js'); + +test('should rotate paths properly', () => { + const svg = ``; + expect( + optimize(svg, { + plugins: ['convertPathData', 'optimizePathOrder'], + }).data + ).toEqual(``); +}); +test('should reverse paths properly', () => { + const svg = ``; + expect( + optimize(svg, { + plugins: ['convertPathData', 'optimizePathOrder'], + }).data + ).toEqual(``); +}); From f6d39064cbaa7d41049b9fede71879dc4909b5cc Mon Sep 17 00:00:00 2001 From: Kendell R Date: Wed, 20 Dec 2023 10:24:53 -0500 Subject: [PATCH 24/32] Make it not on by default --- docs/03-plugins/optimize-path-order.mdx | 1 - plugins/preset-default.js | 2 -- 2 files changed, 3 deletions(-) diff --git a/docs/03-plugins/optimize-path-order.mdx b/docs/03-plugins/optimize-path-order.mdx index 8dd9a3269..f6e88f650 100644 --- a/docs/03-plugins/optimize-path-order.mdx +++ b/docs/03-plugins/optimize-path-order.mdx @@ -2,7 +2,6 @@ title: Optimize Path Order svgo: pluginId: optimizePathOrder - defaultPlugin: true parameters: floatPrecision: description: Number of decimal places to round to, using conventional rounding rules. diff --git a/plugins/preset-default.js b/plugins/preset-default.js index fab6c564f..86516fd72 100644 --- a/plugins/preset-default.js +++ b/plugins/preset-default.js @@ -28,7 +28,6 @@ const moveElemsAttrsToGroup = require('./moveElemsAttrsToGroup.js'); const moveGroupAttrsToElems = require('./moveGroupAttrsToElems.js'); const collapseGroups = require('./collapseGroups.js'); const convertPathData = require('./convertPathData.js'); -const optimizePathOrder = require('./optimizePathOrder.js'); const convertTransform = require('./convertTransform.js'); const removeEmptyAttrs = require('./removeEmptyAttrs.js'); const removeEmptyContainers = require('./removeEmptyContainers.js'); @@ -68,7 +67,6 @@ const presetDefault = createPreset({ moveGroupAttrsToElems, collapseGroups, convertPathData, - optimizePathOrder, convertTransform, removeEmptyAttrs, removeEmptyContainers, From b50a6e7c4a74bc2e1e93e3381bf2e21d4804d16c Mon Sep 17 00:00:00 2001 From: Kendell R Date: Wed, 20 Dec 2023 10:26:23 -0500 Subject: [PATCH 25/32] fix formatting --- plugins/optimizePathOrder.js | 58 +++++++++++++------------- test/plugins/optimizePathOrder.test.js | 4 +- 2 files changed, 31 insertions(+), 31 deletions(-) diff --git a/plugins/optimizePathOrder.js b/plugins/optimizePathOrder.js index f954dd42b..6fea3328e 100644 --- a/plugins/optimizePathOrder.js +++ b/plugins/optimizePathOrder.js @@ -70,7 +70,7 @@ exports.fn = (root, params) => { console.warn( 'optimizePathOrder is enabled, but data from convertPathData is not present. ' + 'If you want to use optimizePathOrder, enable convertPathData and put optimizePathOrder after it. ' + - 'If you do not want to use optimizePathOrder, disable it.' + 'If you do not want to use optimizePathOrder, disable it.', ); return; } @@ -119,7 +119,7 @@ exports.fn = (root, params) => { for (const [i, part] of parts.entries()) { if (part.valid) { const internalData = part.data.filter( - (item) => item.command != 'm' && item.command != 'M' + (item) => item.command != 'm' && item.command != 'M', ); if (internalData.length > 0) { const start = internalData[0].base; @@ -148,13 +148,13 @@ exports.fn = (root, params) => { y2: item.args[3] + item.base[1], } : item.command == 'C' - ? { - x1: item.args[0], - y1: item.args[1], - x2: item.args[2], - y2: item.args[3], - } - : undefined, + ? { + x1: item.args[0], + y1: item.args[1], + x2: item.args[2], + y2: item.args[3], + } + : undefined, q: item.command == 'q' ? { @@ -162,11 +162,11 @@ exports.fn = (root, params) => { y: item.args[1] + item.base[1], } : item.command == 'Q' - ? { - x: item.args[0], - y: item.args[1], - } - : undefined, + ? { + x: item.args[0], + y: item.args[1], + } + : undefined, base: item.base, coords: item.coords, })), @@ -269,8 +269,8 @@ function optimizePart({ { ...item, command: getCommand(item) }, precision, output[output.length - 1].command, - output.length == input.length && !unsafeToChangeStart - ) + output.length == input.length && !unsafeToChangeStart, + ), ); } } else { @@ -291,16 +291,16 @@ function optimizePart({ { ...previousItem, command: getCommand(previousItem) }, precision, output[output.length - 1].command, - false - ) + false, + ), ); output.push( transformCommand( { ...newItem, command: getCommand(newItem) }, precision, output[output.length - 1].command, - !unsafeToChangeStart - ) + !unsafeToChangeStart, + ), ); } i++; @@ -310,7 +310,7 @@ function optimizePart({ (next ? estimateLength( transformMove(next, output[output.length - 1].coords), - precision + precision, ) : 0); if (size < best.size) { @@ -334,10 +334,10 @@ function getCommand(item) { return item.command == 'a' || item.command == 'A' ? 'A' : item.command == 'c' || item.command == 'C' - ? 'C' - : item.command == 'q' || item.command == 'Q' - ? 'Q' - : 'L'; + ? 'C' + : item.command == 'q' || item.command == 'Q' + ? 'Q' + : 'L'; } /** @@ -384,7 +384,7 @@ function transformCommand(command, precision, lastCommand, useZ) { ); const args = [data.x1, data.y1, data.x2, data.y2, ...command.coords]; const argsRelative = args.map((a, i) => - i % 2 == 0 ? a - command.base[0] : a - command.base[1] + i % 2 == 0 ? a - command.base[0] : a - command.base[1], ); const absoluteLength = estimateLength(args, precision) + (lastCommand == 'C' ? 0 : 1); @@ -400,7 +400,7 @@ function transformCommand(command, precision, lastCommand, useZ) { const data = /** @type {{x: number, y: number}} */ (command.q); const args = [data.x, data.y, ...command.coords]; const argsRelative = args.map((a, i) => - i % 2 == 0 ? a - command.base[0] : a - command.base[1] + i % 2 == 0 ? a - command.base[0] : a - command.base[1], ); const absoluteLength = estimateLength(args, precision) + (lastCommand == 'Q' ? 0 : 1); @@ -425,7 +425,7 @@ function transformCommand(command, precision, lastCommand, useZ) { } else if (command.base[1] == command.coords[1]) { const absoluteLength = toPrecision( command.coords[0], - precision + precision, ).toString().length; const relativeLength = toPrecision(relativeX, precision).toString() .length; @@ -439,7 +439,7 @@ function transformCommand(command, precision, lastCommand, useZ) { } else if (command.base[0] == command.coords[0]) { const absoluteLength = toPrecision( command.coords[1], - precision + precision, ).toString().length; const relativeLength = toPrecision(relativeY, precision).toString() .length; diff --git a/test/plugins/optimizePathOrder.test.js b/test/plugins/optimizePathOrder.test.js index 3e2bb44ff..c12cee30f 100644 --- a/test/plugins/optimizePathOrder.test.js +++ b/test/plugins/optimizePathOrder.test.js @@ -7,7 +7,7 @@ test('should rotate paths properly', () => { expect( optimize(svg, { plugins: ['convertPathData', 'optimizePathOrder'], - }).data + }).data, ).toEqual(``); }); test('should reverse paths properly', () => { @@ -15,6 +15,6 @@ test('should reverse paths properly', () => { expect( optimize(svg, { plugins: ['convertPathData', 'optimizePathOrder'], - }).data + }).data, ).toEqual(``); }); From e62675acf255a3c53d8851c64b79d028fc199f51 Mon Sep 17 00:00:00 2001 From: Kendell R Date: Wed, 27 Dec 2023 21:20:46 -0500 Subject: [PATCH 26/32] use new set --- plugins/optimizePathOrder.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/optimizePathOrder.js b/plugins/optimizePathOrder.js index 6fea3328e..475b3f6ec 100644 --- a/plugins/optimizePathOrder.js +++ b/plugins/optimizePathOrder.js @@ -34,7 +34,7 @@ exports.fn = (root, params) => { if (hasScripts(node)) { deoptimized = true; } - if (!pathElems.includes(node.name) || !node.attributes.d || deoptimized) + if (!pathElems.has(node.name) || !node.attributes.d || deoptimized) return; const computedStyle = computeStyle(stylesheet, node); From d065475fcdc57cbba505940eff075c03d078337f Mon Sep 17 00:00:00 2001 From: Kendell R Date: Sat, 6 Jan 2024 07:51:23 -0800 Subject: [PATCH 27/32] update to esm, return early for perf --- plugins/optimizePathOrder.js | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/plugins/optimizePathOrder.js b/plugins/optimizePathOrder.js index 475b3f6ec..92a144ecc 100644 --- a/plugins/optimizePathOrder.js +++ b/plugins/optimizePathOrder.js @@ -1,14 +1,14 @@ -const { computeStyle, collectStylesheet } = require('../lib/style.js'); -const { hasScripts } = require('../lib/svgo/tools.js'); -const { pathElems } = require('./_collections.js'); -const { path2js, js2path } = require('./_path.js'); +import { collectStylesheet, computeStyle } from '../lib/style.js'; +import { hasScripts } from '../lib/svgo/tools.js'; +import { pathElems } from './_collections.js'; +import { js2path, path2js } from './_path.js'; -exports.name = 'optimizePathOrder'; -exports.description = 'Moves around instructions in paths to be optimal.'; +export const name = 'optimizePathOrder'; +export const description = 'Moves around instructions in paths to be optimal.'; /** - * @typedef {import('../lib/types').PathDataCommand} PathDataCommand - * @typedef {import('../lib/types').PathDataItem} PathDataItem + * @typedef {import('../lib/types.js').PathDataCommand} PathDataCommand + * @typedef {import('../lib/types.js').PathDataItem} PathDataItem * @typedef {{command: PathDataCommand, arc?: {rx: number, ry: number, r: number, large: boolean, sweep: boolean}, c?: {x1: number, y1: number, x2: number, y2: number}, q?: {x: number, y: number}, base: [number, number], coords: [number, number]}} InternalPath * @typedef {PathDataItem & {base: [number, number], coords: [number, number]}} RealPath */ @@ -18,9 +18,9 @@ exports.description = 'Moves around instructions in paths to be optimal.'; * * @author Kendell R * - * @type {import('./plugins-types').Plugin<'optimizePathOrder'>} + * @type {import('./plugins-types.js').Plugin<'optimizePathOrder'>} */ -exports.fn = (root, params) => { +export const fn = (root, params) => { const { floatPrecision: precision = 3, noSpaceAfterFlags = false, @@ -77,10 +77,12 @@ exports.fn = (root, params) => { const parts = []; let part = { valid: true, data: /** @type {RealPath[]} */ ([]) }; + let someValid = false; for (const instruction of path) { if (instruction.command == 'M' || instruction.command == 'm') { if (part.data.length > 0) { parts.push(part); + someValid = someValid || part.valid; part = { valid: true, data: [] }; } } @@ -110,12 +112,15 @@ exports.fn = (root, params) => { } if (part.data.length > 0) { parts.push(part); + someValid = someValid || part.valid; } + if (!someValid) return; /** * @type {PathDataItem[]} */ const pathTransformed = []; + let someTransformed = false; for (const [i, part] of parts.entries()) { if (part.valid) { const internalData = part.data.filter( @@ -183,6 +188,7 @@ exports.fn = (root, params) => { }); if (result.success) { pathTransformed.push(...result.data); + someTransformed = true; continue; } } @@ -191,6 +197,7 @@ exports.fn = (root, params) => { pathTransformed.push(...part.data); } + if (!someTransformed) return; js2path(node, pathTransformed, { floatPrecision: precision, noSpaceAfterFlags, From 6d1e7d583f66fef05c583f09074e6b41840eaeaf Mon Sep 17 00:00:00 2001 From: Kendell R Date: Sat, 6 Jan 2024 08:00:38 -0800 Subject: [PATCH 28/32] rearrange statement, use tofixed, fix test --- plugins/optimizePathOrder.js | 31 +++++++------------------- test/plugins/optimizePathOrder.test.js | 4 +--- 2 files changed, 9 insertions(+), 26 deletions(-) diff --git a/plugins/optimizePathOrder.js b/plugins/optimizePathOrder.js index 92a144ecc..948cafd01 100644 --- a/plugins/optimizePathOrder.js +++ b/plugins/optimizePathOrder.js @@ -1,5 +1,5 @@ import { collectStylesheet, computeStyle } from '../lib/style.js'; -import { hasScripts } from '../lib/svgo/tools.js'; +import { hasScripts, toFixed } from '../lib/svgo/tools.js'; import { pathElems } from './_collections.js'; import { js2path, path2js } from './_path.js'; @@ -81,8 +81,8 @@ export const fn = (root, params) => { for (const instruction of path) { if (instruction.command == 'M' || instruction.command == 'm') { if (part.data.length > 0) { - parts.push(part); someValid = someValid || part.valid; + parts.push(part); part = { valid: true, data: [] }; } } @@ -111,8 +111,8 @@ export const fn = (root, params) => { part.data.push(instruction); } if (part.data.length > 0) { - parts.push(part); someValid = someValid || part.valid; + parts.push(part); } if (!someValid) return; @@ -430,12 +430,9 @@ function transformCommand(command, precision, lastCommand, useZ) { coords: command.coords, }; } else if (command.base[1] == command.coords[1]) { - const absoluteLength = toPrecision( - command.coords[0], - precision, - ).toString().length; - const relativeLength = toPrecision(relativeX, precision).toString() + const absoluteLength = toFixed(command.coords[0], precision).toString() .length; + const relativeLength = toFixed(relativeX, precision).toString().length; return { command: absoluteLength < relativeLength ? 'H' : 'h', args: @@ -444,12 +441,9 @@ function transformCommand(command, precision, lastCommand, useZ) { coords: command.coords, }; } else if (command.base[0] == command.coords[0]) { - const absoluteLength = toPrecision( - command.coords[1], - precision, - ).toString().length; - const relativeLength = toPrecision(relativeY, precision).toString() + const absoluteLength = toFixed(command.coords[1], precision).toString() .length; + const relativeLength = toFixed(relativeY, precision).toString().length; return { command: absoluteLength < relativeLength ? 'V' : 'v', args: @@ -485,15 +479,6 @@ function transformMove(command, newBase) { return [command.coords[0] - newBase[0], command.coords[1] - newBase[1]]; } -/** - * @param {number} number - * @param {number} precision - */ -function toPrecision(number, precision) { - const factor = Math.pow(10, precision); - return Math.round(number * factor) / factor; -} - /** * @param {number[]} numbers * @param {number} precision @@ -502,7 +487,7 @@ function estimateLength(numbers, precision) { let length = 0; let last = undefined; for (const number of numbers) { - const rounded = toPrecision(number, precision); + const rounded = toFixed(number, precision); const string = rounded.toString(); length += string.length - (rounded != 0 && rounded > -1 && rounded < 1 ? 1 : 0); diff --git a/test/plugins/optimizePathOrder.test.js b/test/plugins/optimizePathOrder.test.js index c12cee30f..8a52be008 100644 --- a/test/plugins/optimizePathOrder.test.js +++ b/test/plugins/optimizePathOrder.test.js @@ -1,6 +1,4 @@ -'use strict'; - -const { optimize } = require('../../lib/svgo.js'); +import { optimize } from '../../lib/svgo.js'; test('should rotate paths properly', () => { const svg = ``; From ea8237f1610720c5c17dd4b862b5ffff61e534bb Mon Sep 17 00:00:00 2001 From: Kendell R Date: Sat, 6 Jan 2024 08:35:40 -0800 Subject: [PATCH 29/32] more efficient estimatepathlength --- plugins/optimizePathOrder.js | 59 ++++++++++++++++++------------------ 1 file changed, 30 insertions(+), 29 deletions(-) diff --git a/plugins/optimizePathOrder.js b/plugins/optimizePathOrder.js index 948cafd01..ca9160410 100644 --- a/plugins/optimizePathOrder.js +++ b/plugins/optimizePathOrder.js @@ -510,40 +510,41 @@ function estimateLength(numbers, precision) { * @param {boolean} first */ function estimatePathLength(data, precision, first) { - /** - * @type {{command: string, args: number[]}[]} - */ - let combined = []; - data.forEach((command, i) => { - const last = combined[combined.length - 1]; - if (last) { - let commandless = - (last.command == command.command && - last.command != 'M' && - last.command != 'm') || - (last.command == 'M' && command.command == 'L') || - (last.command == 'm' && command.command == 'l'); + let i = 0; + let length = 0; + while (i < data.length) { + let { command, args } = data[i]; + const isVeryFirst = i == 0 && first; + + let joined = false; + do { + const next = data[i + 1]; + if (!next) break; + + joined = false; + if (command == 'M') { + if (next.command == 'L') joined = true; + } else if (command == 'm') { + if (next.command == 'l') joined = true; + } else { + if (next.command == command) joined = true; + } if ( - first && - i == 1 && - (last.command == 'M' || last.command == 'm') && - (command.command == 'L' || command.command == 'l') + isVeryFirst && + (command == 'M' || command == 'm') && + (next.command == 'L' || next.command == 'l') ) { - commandless = true; - last.command = command.command == 'L' ? 'M' : 'm'; + joined = true; } - if (commandless) { - last.args = [...last.args, ...command.args]; - return; + + if (joined) { + args = [...args, ...next.args]; + i++; } - } - combined.push({ command: command.command, args: command.args }); - }); + } while (joined); - let length = 0; - for (const command of combined) { - length += 1 + estimateLength(command.args, precision); + length += 1 + estimateLength(args, precision); + i++; } - return length; } From 9b8e1e396392f9a049f9a39852430cd1c74ea02e Mon Sep 17 00:00:00 2001 From: Kendell R Date: Sat, 6 Jan 2024 08:57:45 -0800 Subject: [PATCH 30/32] marginally improve estimatelength perf --- plugins/optimizePathOrder.js | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/plugins/optimizePathOrder.js b/plugins/optimizePathOrder.js index ca9160410..8aefb738a 100644 --- a/plugins/optimizePathOrder.js +++ b/plugins/optimizePathOrder.js @@ -491,15 +491,12 @@ function estimateLength(numbers, precision) { const string = rounded.toString(); length += string.length - (rounded != 0 && rounded > -1 && rounded < 1 ? 1 : 0); - if (last) { - if ( - !(rounded < 0) && - !(last.includes('.') && rounded > 0 && rounded < 1) - ) { + if (last !== undefined) { + if (!(rounded < 0) && !(last % 1 && rounded > 0 && rounded < 1)) { length += 1; } } - last = string; + last = rounded; } return length; } From 462b7c0287e096cf78c3fcc5b040778483a1caa1 Mon Sep 17 00:00:00 2001 From: Kendell R Date: Sat, 6 Jan 2024 09:32:02 -0800 Subject: [PATCH 31/32] skip on totally unsafe --- plugins/optimizePathOrder.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plugins/optimizePathOrder.js b/plugins/optimizePathOrder.js index 8aefb738a..6b53b1102 100644 --- a/plugins/optimizePathOrder.js +++ b/plugins/optimizePathOrder.js @@ -227,6 +227,8 @@ function optimizePart({ baseline, precision, }) { + if (unsafeToChangeDirection && unsafeToChangeStart) return { success: false }; + let best = { success: false, size: From 767536979c45c5a3a933aeb5b248b69f35240241 Mon Sep 17 00:00:00 2001 From: Kendell R Date: Sat, 6 Jan 2024 09:37:08 -0800 Subject: [PATCH 32/32] fix ts issue --- plugins/optimizePathOrder.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/plugins/optimizePathOrder.js b/plugins/optimizePathOrder.js index 6b53b1102..2199958c1 100644 --- a/plugins/optimizePathOrder.js +++ b/plugins/optimizePathOrder.js @@ -227,7 +227,9 @@ function optimizePart({ baseline, precision, }) { - if (unsafeToChangeDirection && unsafeToChangeStart) return { success: false }; + if (unsafeToChangeDirection && unsafeToChangeStart) { + return /** @type {const} */ ({ success: false }); + } let best = { success: false,