{
"extension": ".js",
"source": "import { DEFAULT_PRECISION } from \"./_constants\"\nimport { convertPathToNode } from \"./_utils\"\nimport Sample from \"./Sample\"\nimport Segment from \"./Segment\"\n\n// The main function responsible for getting data\n// This will take a path, number of samples, number of samples, and a precision value\n// It will return an array of Segments, which in turn contains an array of Samples\n// This can later be used to generate a stroked path, converted to outlines for a filled path, or flattened for plotting SVG circles\nexport const getData = ({\n path,\n segments,\n samples,\n precision = DEFAULT_PRECISION,\n}) => {\n // Convert the given path to a DOM node if it isn't already one\n path = convertPathToNode(path)\n\n // We decrement the number of samples per segment because when we group them later we will add on the first sample of the following segment\n if (samples > 1) samples--\n\n // Get total length of path, total number of samples we will be generating, and two blank arrays to hold samples and segments\n const pathLength = path.getTotalLength(),\n totalSamples = segments * samples,\n allSamples = [],\n allSegments = []\n\n // For the number of total samples, get the x, y, and progress values for each sample along the path\n for (let sample = 0; sample <= totalSamples; sample++) {\n const progress = sample / totalSamples\n\n let { x, y } = path.getPointAtLength(progress * pathLength)\n\n // If the user asks to round our x and y values, do so\n if (precision) {\n x = +x.toFixed(precision)\n y = +y.toFixed(precision)\n }\n\n // Create a new Sample and push it onto the allSamples array\n allSamples.push(new Sample({ x, y, progress }))\n }\n\n // Out of all the samples gathered previously, sort them into groups of segments\n // Each group includes the samples of the current segment, with the last sample being first sample from the next segment\n for (let segment = 0; segment < segments; segment++) {\n const currentStart = segment * samples,\n nextStart = currentStart + samples,\n segmentSamples = []\n\n // Push all current samples onto segmentSamples\n for (let samInSeg = 0; samInSeg < samples; samInSeg++) {\n segmentSamples.push(allSamples[currentStart + samInSeg])\n }\n\n // Push the first sample from the next segment onto segmentSamples\n segmentSamples.push(allSamples[nextStart])\n\n // Create a new Segment with the samples from segmentSamples\n allSegments.push(new Segment({ samples: segmentSamples }))\n }\n\n // Return our group of segments\n return allSegments\n}\n\n// The function responsible for converting strokable data (from getData()) into fillable data\n// This allows any SVG path to be filled instead of just stroked, allowing for the user to fill and stroke paths simultaneously\n// We start by outlining the stroked data given a specified width and the we average together the edges where adjacent segments touch\nexport const strokeToFill = (data, width, precision, pathClosed) => {\n const outlinedStrokes = outlineStrokes(data, width, precision),\n averagedSegmentJoins = averageSegmentJoins(\n outlinedStrokes,\n precision,\n pathClosed\n )\n\n return averagedSegmentJoins\n}\n\n// An internal function for outlining stroked data\nconst outlineStrokes = (data, width, precision) => {\n // We need to get the points perpendicular to a startPoint, given an angle, radius, and precision\n const getPerpSamples = (angle, radius, precision, startPoint) => {\n const p0 = new Sample({\n ...startPoint,\n x: Math.sin(angle) * radius + startPoint.x,\n y: -Math.cos(angle) * radius + startPoint.y,\n }),\n p1 = new Sample({\n ...startPoint,\n x: -Math.sin(angle) * radius + startPoint.x,\n y: Math.cos(angle) * radius + startPoint.y,\n })\n\n // If the user asks to round our x and y values, do so\n if (precision) {\n p0.x = +p0.x.toFixed(precision)\n p0.y = +p0.y.toFixed(precision)\n p1.x = +p1.x.toFixed(precision)\n p1.y = +p1.y.toFixed(precision)\n }\n\n return [p0, p1]\n }\n\n // We need to set the radius (half of the width) and have a holding array for outlined Segments\n const radius = width / 2,\n outlinedData = []\n\n for (let i = 0; i < data.length; i++) {\n const samples = data[i].samples,\n segmentSamples = []\n\n // For each sample point and the following sample point (if there is one) compute the angle\n // Also compute the sample's various perpendicular points (with a distance of radius away from the sample point)\n for (let j = 0; j < samples.length; j++) {\n // If we're at the end of the segment and there are no further points, get outta here!\n if (samples[j + 1] === undefined) break\n\n const p0 = samples[j], // First point\n p1 = samples[j + 1], // Second point\n angle = Math.atan2(p1.y - p0.y, p1.x - p0.x), // Perpendicular angle to p0 and p1\n p0Perps = getPerpSamples(angle, radius, precision, p0), // Get perpedicular points with a distance of radius away from p0\n p1Perps = getPerpSamples(angle, radius, precision, p1) // Get perpedicular points with a distance of radius away from p1\n\n // We only need the p0 perpendenciular points for the first sample\n // The p0 for j > 0 will always be the same as p1 anyhow, so let's not add redundant points\n if (j === 0) {\n segmentSamples.push(...p0Perps)\n }\n\n // Always push the second sample point's perpendicular points\n segmentSamples.push(...p1Perps)\n }\n\n // segmentSamples is out of order...\n // Given a segmentSamples length of 8, the points need to be rearranged from: 0, 2, 4, 6, 7, 5, 3, 1\n outlinedData.push(\n new Segment({\n samples: [\n ...segmentSamples.filter((s, i) => i % 2 === 0),\n ...segmentSamples.filter((s, i) => i % 2 === 1).reverse(),\n ],\n })\n )\n }\n\n return outlinedData\n}\n\n// An internal function taking outlinedData (from outlineStrokes()) and averaging adjacent edges\n// If we didn't do this, our data would be fillable, but it would look stroked\n// This function fixes where segments overlap and underlap each other\nconst averageSegmentJoins = (outlinedData, precision, pathClosed) => {\n // Find the average x and y between two points (p0 and p1)\n const avg = (p0, p1) => ({\n x: (p0.x + p1.x) / 2,\n y: (p0.y + p1.y) / 2,\n })\n\n // Recombine the new x and y positions with all the other keys in the object\n const combine = (segment, pos, avg) => ({\n ...segment[pos],\n x: avg.x,\n y: avg.y,\n })\n\n const init_outlinedData = JSON.parse(JSON.stringify(outlinedData)) //clone initial outlinedData Object\n\n for (let i = 0; i < outlinedData.length; i++) {\n // If path is closed: the current segment's samples;\n // If path is open: the current segments' samples, as long as it's not the last segment; Otherwise, the current segments' sample of the initial outlinedData object\n const currentSamples = pathClosed\n ? outlinedData[i].samples\n : outlinedData[i + 1]\n ? outlinedData[i].samples\n : init_outlinedData[i].samples,\n // If path is closed: the next segment's samples, otherwise, the first segment's samples\n // If path is open: the next segment's samples, otherwise, the first segment's samples of the initial outlinedData object\n nextSamples = pathClosed\n ? outlinedData[i + 1]\n ? outlinedData[i + 1].samples\n : outlinedData[0].samples\n : outlinedData[i + 1]\n ? outlinedData[i + 1].samples\n : init_outlinedData[0].samples,\n currentMiddle = currentSamples.length / 2, // The \"middle\" sample in the current segment's samples\n nextEnd = nextSamples.length - 1 // The last sample in the next segment's samples\n\n // Average two sets of outlined samples to create p0Average and p1Average\n const p0Average = avg(currentSamples[currentMiddle - 1], nextSamples[0]),\n p1Average = avg(currentSamples[currentMiddle], nextSamples[nextEnd])\n\n // If the user asks to round our x and y values, do so\n if (precision) {\n p0Average.x = +p0Average.x.toFixed(precision)\n p0Average.y = +p0Average.y.toFixed(precision)\n p1Average.x = +p1Average.x.toFixed(precision)\n p1Average.y = +p1Average.y.toFixed(precision)\n }\n\n // Replace the previous values with new Samples\n currentSamples[currentMiddle - 1] = new Sample({\n ...combine(currentSamples, currentMiddle - 1, p0Average),\n })\n currentSamples[currentMiddle] = new Sample({\n ...combine(currentSamples, currentMiddle, p1Average),\n })\n nextSamples[0] = new Sample({\n ...combine(nextSamples, 0, p0Average),\n })\n nextSamples[nextEnd] = new Sample({\n ...combine(nextSamples, nextEnd, p1Average),\n })\n }\n\n return outlinedData\n}\n",
"review": "# 7\n\n## Bugs\n- No validation of input parameters like `path`, which could lead to potential runtime errors if incorrect data types are provided.\n- Possibility of division by zero in `getData` if `segments` or `samples` is zero, which could raise an exception.\n- Assumes `path` has defined methods like `getTotalLength` without checking if they exist or handling errors if they're not available.\n- In `averageSegmentJoins`, there's potential risk of accessing undefined array elements if `outlinedData` length is less than expected, particularly in loop iterations.\n\n## Optimizations\n- Consider adding validation checks for input parameters (e.g., ensuring `segments` and `samples` are positive numbers).\n- Avoid unnecessary calculations by short-circuiting conditions when precision is not needed.\n- Use `Object.assign()` or spread operator to clone objects instead of JSON stringification and parsing for better performance.\n- Reduce redundancy by extracting repetitive logic, such as rounding and averaging, into separate utility functions.\n- Consider using more descriptive parameter names to enhance code readability.\n\n## Good points\n- Implements a clear transformation of path data to samples and segments, showing a logical step-by-step process.\n- Attempts to handle precision in the calculations, offering flexibility to the caller.\n- Thoroughly comments the purpose and process of each function and code block, aiding in comprehension.\n\n## Summary\nThe code effectively maps path data into segmented samples with an option for precision. It demonstrates consistent approach in processing data, maintaining modularity with separate functions for distinct tasks. However, this implementation lacks input validation, which can cause runtime issues with unexpected inputs. By addressing potential errors, optimizing code redundancy, and ensuring input validity, the solution can be made more robust and efficient.\n\n## Open source alternatives\n- Snap.svg: A JavaScript library for modern SVG graphics focusing on delivering faster performance and better optimization.\n- D3.js: While primarily focused on data visualization, it includes powerful utilities for manipulating documents based on data, including path generation and modification.",
"filename": "_data.js",
"path": "remix/app/gp/_data.js",
"directory": "gp",
"grade": 7,
"size": 8782,
"line_count": 220
}