diff --git a/src/path.mjs b/src/path.mjs index 3b0b6678..0820a8ef 100644 --- a/src/path.mjs +++ b/src/path.mjs @@ -32,8 +32,9 @@ function roundDecimal(float, places) { const roundedDecimalPart = decimalRoundingCache[places][decimalPart]; return integerPart + roundedDecimalPart; } - - const roundedDecimalPart = +(Math.round(decimalPart + 'e+' + places) + 'e-' + places); + + const roundedDecimalPart = Number((Math.round(decimalPart * Math.pow(10, places)) / Math.pow(10, places)).toFixed(places)); + decimalRoundingCache[places][decimalPart] = roundedDecimalPart; return integerPart + roundedDecimalPart; @@ -52,7 +53,7 @@ function optimizeCommands(commands) { const previousCommand = subpath[subpath.length - 1]; const nextCommand = commands[i + 1]; subpath.push(cmd); - + if (cmd.type === 'M') { startX = cmd.x; startY = cmd.y; @@ -713,7 +714,7 @@ Path.prototype.toDOMElement = function(options, pathData) { newPath.setAttribute('fill', this.fill); } } - + if (this.stroke) { newPath.setAttribute('stroke', this.stroke); newPath.setAttribute('stroke-width', this.strokeWidth); diff --git a/test/path.spec.mjs b/test/path.spec.mjs index 8bcc7e79..2fcfeba2 100644 --- a/test/path.spec.mjs +++ b/test/path.spec.mjs @@ -6,7 +6,7 @@ describe('path.mjs', function() { let testPath1; let testPath2; - + beforeEach(function() { global.document = { createElementNS: function (namespace, tagName) { @@ -23,14 +23,14 @@ describe('path.mjs', function() { }; emptyPath = new Path(); - + testPath1 = new Path(); testPath1.moveTo(1, 2); testPath1.lineTo(3, 4); testPath1.curveTo(5, 6, 7, 8, 9, 10); testPath1.quadTo(11, 12, 13, 14, 15, 16); testPath1.close(); - + testPath2 = new Path(); // two squares testPath2.moveTo(0, 50); testPath2.lineTo(0, 250); @@ -51,7 +51,7 @@ describe('path.mjs', function() { testPath2.lineTo(250, 50); testPath2.close(); }); - + it('should set path commands correctly', function() { const expectedCommands = [ { type: 'M', x: 1, y: 2 }, @@ -64,14 +64,14 @@ describe('path.mjs', function() { assert.deepEqual(testPath1.commands, expectedCommands); assert.deepEqual(Path.fromSVG(svg, {flipY: false}).commands, expectedCommands); }); - + it('should return a streamlined SVG path (no commas, no additional spaces, only absolute commands)', function() { const input = 'M1,2 L 3 4Z M .5 6.7 L 8 9 l 2,1 m1 1 c 1 2,3 4 5, 6q-7.8-9.0 -1.011 12 m-13.99-28 h 13 15 V 17 19 v21 23 25 H27 V28 zzzZZzzz'; const expectedSVG = 'M1 2L3 4ZM0.50 6.70L8 9L10 10M11 11C12 13 14 15 16 17Q8.20 8 14.99 29M1 1L14 1L29 1L29 17L29 19L29 40L29 63L29 88L27 88L27 28Z'; const path = Path.fromSVG(input, {flipY: false}); assert.deepEqual(path.toPathData({flipY: false}), expectedSVG); }); - + it('should accept integer or correct fallback for decimalPlaces backwards compatibility', function() { const expectedResult = 'M0.58 0.75L1.76-1.25'; const expectedResult2 = 'M0.575 0.750L1.757-1.254'; @@ -82,18 +82,18 @@ describe('path.mjs', function() { assert.equal(path.toPathData({optimize: true, flipY: false}), expectedResult); assert.equal(path.toPathData(3), expectedResult2); }); - + it('should not optimize SVG paths if parameter is set falsy', function() { const unoptimizedResult = 'M0 50L0 250L50 250L100 250L150 250L200 250L200 50L0 50ZM250 50L250 250L300 250L350 250L400 250L450 250L450 50L250 50Z'; assert.equal(testPath2.toPathData({optimize: false, flipY: false}), unoptimizedResult); }); - + it('should optimize SVG paths if path closing point matches starting point', function() { const optimizedResult = 'M0 250L50 250L100 250L150 250L200 250L200 50L0 50ZM250 250L300 250L350 250L400 250L450 250L450 50L250 50Z'; assert.equal(testPath2.toPathData({flipY: false}), optimizedResult); assert.equal(testPath2.toPathData({optimize: true, flipY: false}), optimizedResult); }); - + it('should optimize SVG paths if they include unnecessary lineTo commands', function() { const path = (new Path()).fromSVG( 'M199 97 L 199 97 L 313 97 L 313 97 Q 396 97 444 61 L 444 61 L 444 61 Q 493 25 493 -36 L 493 -36 L 493 -36' + @@ -104,7 +104,7 @@ describe('path.mjs', function() { assert.equal(path.toSVG({optimize: true}), expectedResult); assert.equal(path.toDOMElement({optimize: true}).getAttribute('d'), expectedPath); }); - + it('should calculate flipY from bounding box if set to true', function() { const jNormal = 'M25 772C130 725 185 680 185 528L185 33L93 33L93 534C93 647 60 673-9 705ZM204-150' + 'C204-185 177-212 139-212C101-212 75-185 75-150C75-114 101-87 139-87C177-87 204-114 204-150Z'; @@ -114,14 +114,14 @@ describe('path.mjs', function() { assert.equal(path.toPathData({flipY: false}), jUpsideDown); assert.equal(path.toPathData(), jNormal); }); - + it('should handle scaling and offset', function() { const inputPath = 'M0 1L2 0L3 0L5 1L5 5L0 5Z'; const expectedPath = 'M1 4.50L6 2L8.50 2L13.50 4.50L13.50 14.50L1 14.50Z'; const path = Path.fromSVG(inputPath, { x: 1, y: 2, scale: 2.5 }); assert.equal(path.toPathData(), expectedPath); }); - + it('should apply fill and stroke for toSVG()', function() { assert.equal(emptyPath.toSVG(), ''); emptyPath.fill = '#ffaa00'; @@ -135,13 +135,13 @@ describe('path.mjs', function() { emptyPath.fill = 'black'; assert.equal(emptyPath.toSVG(), ''); }); - + it('should apply fill and stroke for toDOMElement()', function() { // in browser context these wouldn't be undefined, but we're only mocking it assert.equal(emptyPath.toDOMElement().getAttribute('fill'), undefined); assert.equal(emptyPath.toDOMElement().getAttribute('stroke'), undefined); assert.equal(emptyPath.toDOMElement().getAttribute('stroke-width'), undefined); - + emptyPath.fill = '#ffaa00'; assert.equal(emptyPath.toDOMElement().getAttribute('fill'), '#ffaa00'); emptyPath.stroke = '#0000ff'; @@ -154,7 +154,30 @@ describe('path.mjs', function() { emptyPath.fill = 'black'; assert.equal(emptyPath.toDOMElement().getAttribute('fill'), undefined); }); - + + it('should not return NaNs in paths', function() { + let expectedResult = 'M0 349'; + let path = new Path(); + path.moveTo(0, 349.00000000000006); + assert.equal(path.toPathData({decimalPlaces: 2}), expectedResult); + + expectedResult = 'M0 349.01'; + path = new Path(); + path.moveTo(0, 349.0060000000000); + assert.equal(path.toPathData({decimalPlaces: 2}), expectedResult); + + expectedResult = 'M0 349.06'; + path = new Path(); + path.moveTo(0, 349.060000000000); + assert.equal(path.toPathData({decimalPlaces: 2}), expectedResult); + + expectedResult = 'M0 349.00000000000006'; + path = new Path(); + path.moveTo(0, 349.00000000000006); + assert.equal(path.toPathData({decimalPlaces: 14}), expectedResult); + + }); + afterEach(() => { delete global.document; });