svg-path-simplify 0.0.1 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +28 -1
  2. package/dist/svg-path-simplify.esm.js +4040 -0
  3. package/dist/svg-path-simplify.esm.min.js +1 -0
  4. package/dist/svg-path-simplify.js +4065 -0
  5. package/dist/svg-path-simplify.min.js +1 -0
  6. package/dist/svg-path-simplify.node.js +4062 -0
  7. package/dist/svg-path-simplify.node.min.js +1 -0
  8. package/index.html +222 -0
  9. package/package.json +2 -2
  10. package/src/constants.js +4 -0
  11. package/src/index.js +18 -3
  12. package/src/pathData_simplify_cubic.js +324 -0
  13. package/src/pathData_simplify_cubic_arr.js +50 -0
  14. package/src/pathData_simplify_cubic_extrapolate.js +220 -0
  15. package/src/pathSimplify-main.js +294 -0
  16. package/src/svgii/...parse.js +402 -0
  17. package/src/svgii/geometry.js +1096 -0
  18. package/src/svgii/geometry_area.js +265 -0
  19. package/src/svgii/geometry_bbox.js +223 -0
  20. package/src/svgii/pathData_analyze.js +896 -0
  21. package/src/svgii/pathData_convert.js +1180 -0
  22. package/src/svgii/pathData_parse.js +487 -0
  23. package/src/svgii/pathData_remove_collinear.js +85 -0
  24. package/src/svgii/pathData_remove_zerolength.js +28 -0
  25. package/src/svgii/pathData_reorder.js +204 -0
  26. package/src/svgii/pathData_reverse.js +124 -0
  27. package/src/svgii/pathData_scale.js +42 -0
  28. package/src/svgii/pathData_split.js +449 -0
  29. package/src/svgii/pathData_stringify.js +146 -0
  30. package/src/svgii/pathData_toPolygon.js +92 -0
  31. package/src/svgii/pathdata_cleanup.js +363 -0
  32. package/src/svgii/poly_analyze.js +172 -0
  33. package/src/svgii/poly_to_pathdata.js +185 -0
  34. package/src/svgii/rounding.js +154 -0
  35. package/src/svgii/simplify.js +248 -0
  36. package/src/svgii/simplify_bezier.js +470 -0
  37. package/src/svgii/simplify_linetos.js +93 -0
  38. package/src/svgii/simplify_polygon.js +135 -0
  39. package/src/svgii/stringify.js +103 -0
  40. package/src/svgii/svg_cleanup.js +80 -0
  41. package/src/svgii/visualize.js +317 -0
  42. package/LICENSE +0 -21
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Serialize pathData array to a minified "d" attribute string.
3
+ */
4
+ export function pathDataToD(pathData, optimize = 1) {
5
+
6
+ let beautify = optimize>1;
7
+ let minify = beautify ? false : true;
8
+
9
+ // Convert first "M" to "m" if followed by "l" (when minified)
10
+ if (pathData[1].type === "l" && minify) {
11
+ pathData[0].type = "m";
12
+ }
13
+
14
+ let d = '';
15
+ if(beautify) {
16
+ d = `${pathData[0].type} ${pathData[0].values.join(" ")}\n`;
17
+ }else{
18
+ d = `${pathData[0].type}${pathData[0].values.join(" ")}`;
19
+ }
20
+
21
+
22
+ for (let i = 1, len = pathData.length; i < len; i++) {
23
+ let com0 = pathData[i - 1];
24
+ let com = pathData[i];
25
+ let { type, values } = com;
26
+
27
+ // Minify Arc commands (A/a) – actually sucks!
28
+ if (minify && (type === 'A' || type === 'a')) {
29
+ values = [
30
+ values[0], values[1], values[2],
31
+ `${values[3]}${values[4]}${values[5]}`,
32
+ values[6]
33
+ ];
34
+ }
35
+
36
+ // Omit type for repeated commands
37
+ type = (com0.type === com.type && com.type.toLowerCase() !== 'm' && minify)
38
+ ? " "
39
+ : (
40
+ (com0.type === "m" && com.type === "l") ||
41
+ (com0.type === "M" && com.type === "l") ||
42
+ (com0.type === "M" && com.type === "L")
43
+ ) && minify
44
+ ? " "
45
+ : com.type;
46
+
47
+
48
+ // concatenate subsequent floating point values
49
+ if (minify) {
50
+
51
+ //console.log(optimize, beautify, minify);
52
+
53
+ let valsString = '';
54
+ let prevWasFloat = false;
55
+
56
+ for (let v = 0, l = values.length; v < l; v++) {
57
+ let val = values[v];
58
+ let valStr = val.toString();
59
+ let isFloat = valStr.includes('.');
60
+ let isSmallFloat = isFloat && Math.abs(val) < 1;
61
+
62
+
63
+ // Remove leading zero from small floats *only* if the previous was also a float
64
+ if (isSmallFloat && prevWasFloat) {
65
+ valStr = valStr.replace(/^0\./, '.');
66
+ }
67
+
68
+ // Add space unless this is the first value OR previous was a small float
69
+ if (v > 0 && !(prevWasFloat && isSmallFloat)) {
70
+ valsString += ' ';
71
+ }
72
+ //console.log(isSmallFloat, prevWasFloat, valStr);
73
+
74
+ valsString += valStr
75
+ //.replace(/-0./g, '-.').replace(/ -./g, '-.')
76
+ prevWasFloat = isSmallFloat;
77
+ }
78
+
79
+ //console.log('minify', valsString);
80
+ d += `${type}${valsString}`;
81
+
82
+ }
83
+ // regular non-minified output
84
+ else{
85
+ if(beautify) {
86
+ d += `${type} ${values.join(' ')}\n`;
87
+ }else{
88
+ d += `${type}${values.join(' ')}`;
89
+ }
90
+ }
91
+ }
92
+
93
+ if (minify) {
94
+ d = d
95
+ .replace(/ 0\./g, " .") // Space before small decimals
96
+ .replace(/ -/g, "-") // Remove space before negatives
97
+ .replace(/-0\./g, "-.") // Remove leading zero from negative decimals
98
+ .replace(/Z/g, "z"); // Convert uppercase 'Z' to lowercase
99
+ }
100
+
101
+
102
+ return d;
103
+ }
@@ -0,0 +1,80 @@
1
+ export function cleanUpSVG(svgMarkup, removeHidden=true) {
2
+ svgMarkup = cleanSvgPrologue(svgMarkup);
3
+
4
+ // replace namespaced refs
5
+ svgMarkup = svgMarkup.replaceAll("xlink:href=", "href=");
6
+
7
+ let svg = new DOMParser()
8
+ .parseFromString(svgMarkup, "text/html")
9
+ .querySelector("svg");
10
+
11
+
12
+ let allowed=['viewBox', 'xmlns', 'width', 'height', 'id', 'class'];
13
+ removeExcludedAttribues(svg, allowed)
14
+
15
+ let removeEls = ['metadata', 'script']
16
+
17
+ let els = svg.querySelectorAll('*')
18
+ els.forEach(el=>{
19
+ let name = el.nodeName;
20
+ // remove hidden elements
21
+ let style = el.getAttribute('style') || ''
22
+ let isHiddenByStyle = style ? style.trim().includes('display:none') : false;
23
+ let isHidden = (el.getAttribute('display') && el.getAttribute('display') === 'none') || isHiddenByStyle;
24
+ if(name.includes(':') || removeEls.includes(name) || (removeHidden && isHidden )) {
25
+ el.remove();
26
+ }else{
27
+ // remove BS elements
28
+ removeNameSpaceAtts(el)
29
+ }
30
+ })
31
+
32
+ let markup = stringifySVG(svg)
33
+ console.log(markup);
34
+
35
+ return markup;
36
+ }
37
+
38
+ function cleanSvgPrologue(svgString) {
39
+ return (
40
+ svgString
41
+ // Remove XML prologues like <?xml ... ?>
42
+ .replace(/<\?xml[\s\S]*?\?>/gi, "")
43
+ // Remove DOCTYPE declarations
44
+ .replace(/<!DOCTYPE[\s\S]*?>/gi, "")
45
+ // Remove comments <!-- ... -->
46
+ .replace(/<!--[\s\S]*?-->/g, "")
47
+ // Trim extra whitespace
48
+ .trim()
49
+ );
50
+ }
51
+
52
+ function removeExcludedAttribues(el, allowed=['viewBox', 'xmlns', 'width', 'height', 'id', 'class']){
53
+ let atts = [...el.attributes].map((att) => att.name);
54
+ atts.forEach((att) => {
55
+ if (!allowed.includes(att)) {
56
+ el.removeAttribute(att);
57
+ }
58
+ });
59
+ }
60
+
61
+
62
+ function removeNameSpaceAtts(el) {
63
+ let atts = [...el.attributes].map((att) => att.name);
64
+ atts.forEach((att) => {
65
+ if (att.includes(":")) {
66
+ el.removeAttribute(att);
67
+ }
68
+ });
69
+ }
70
+
71
+ function stringifySVG(svg){
72
+ let markup = new XMLSerializer().serializeToString(svg);
73
+ markup = markup
74
+ .replace(/\t/g, "")
75
+ .replace(/[\n\r|]/g, "\n")
76
+ .replace(/\n\s*\n/g, '\n')
77
+ .replace(/ +/g, ' ')
78
+
79
+ return markup
80
+ }
@@ -0,0 +1,317 @@
1
+
2
+ //import { splitSubpaths } from "./pathData_split";
3
+
4
+ //import { getPathDataVertices } from "./geometry";
5
+ //import { getPolygonArea } from "./geometry_area";
6
+
7
+ export function renderCommands(pathData, target = 'svg1') {
8
+
9
+ const splitSubpaths = (pathData) => {
10
+ let subPathArr = [];
11
+
12
+ //split segments after M command
13
+ let subPathIndices = pathData.map((com, i) => (com.type.toLowerCase() === 'm' ? i : -1)).filter(i => i !== -1);
14
+
15
+ // no compound path
16
+ if (subPathIndices.length === 1) {
17
+ return [pathData]
18
+ }
19
+ subPathIndices.forEach((index, i) => {
20
+ subPathArr.push(pathData.slice(index, subPathIndices[i + 1]));
21
+ });
22
+
23
+ return subPathArr;
24
+ }
25
+
26
+ //parent group
27
+ let svg_g_top = `<g class="cpt_g cpt_g_top">`;
28
+ let svg_g = `<g class="cpt_g">`;
29
+
30
+ svg_g += `
31
+
32
+ <style>
33
+
34
+ .cpt_handle {
35
+ stroke: #ccc;
36
+ }
37
+
38
+ .cpt_g {
39
+ stroke-width: 0.3%;
40
+ color: red;
41
+ }
42
+
43
+ .cpt_handle {
44
+ stroke: currentColor;
45
+ }
46
+
47
+ .cpt_handle_tip {
48
+ r: 1%;
49
+ fill: currentColor;
50
+ }
51
+
52
+ .cpt_handle_tip_m {
53
+ r: 1.5%;
54
+ display:none;
55
+ fill: transparent;
56
+ stroke: currentColor;
57
+ }
58
+ </style>
59
+
60
+
61
+ <symbol id="ccw_circle" viewBox="-5 -5 110 110" overflow="visible">
62
+ <path fill="none" stroke="currentColor"
63
+ d="M 4 31 c 8 -19 26 -31 46 -31 c 28 0 50 22 50 50 c 0 28 -22 50 -50 50 c -14 0 -36 -6 -46 -31
64
+ m -2 -76 v 37 h 37
65
+ "/>
66
+ </symbol>
67
+
68
+ <symbol id="cw_circle" viewBox="-5 -5 110 110" overflow="visible">
69
+ <path fill="none" stroke="currentColor" transform="scale(-1 1)" transform-origin="center"
70
+ d="M 4 31 c 8 -19 26 -31 46 -31 c 28 0 50 22 50 50 c 0 28 -22 50 -50 50 c -14 0 -36 -6 -46 -31
71
+ m -2 -76 v 37 h 37
72
+ "/>
73
+ </symbol>
74
+
75
+
76
+ `;
77
+
78
+
79
+
80
+ //split in sub paths
81
+ let pathDataArr = splitSubpaths(pathData);
82
+ let M, cp1, cp2, cp1_0, cp2_0, p0, p;
83
+
84
+ pathDataArr.forEach((sub, s) => {
85
+
86
+ M = { x: sub[0].values[0], y: sub[0].values[1] }
87
+ p0 = M
88
+
89
+ let svg = `<g class="cpt_g">`;
90
+ let svg_cpts_top = ``
91
+
92
+ let polyPts = getPathDataVertices(sub);
93
+ let area = getPolygonArea(polyPts);
94
+ let cw = area<0 ? false : true;
95
+
96
+
97
+ sub.forEach((com, i) => {
98
+ let { type, values } = com;
99
+
100
+ if (type === 'Z') {
101
+ //p = M
102
+ //renderPoint(svg1, p, 'red', '2%')
103
+ } else {
104
+
105
+ let valsL = type != 'V' && type != 'H' ? values.slice(-2) :
106
+ (type === 'V' ? [p0.x, values[0]] : [values[0], p0.y]);
107
+ p = { x: valsL[0], y: valsL[1] };
108
+ //renderPoint(svg1, p, 'green', '3%')
109
+
110
+ let transCW = cw ? 'scale(-1 1)' : 'scale(1 1)';
111
+
112
+ if (type === 'M') {
113
+
114
+ //<use href="#ccw_circle" stroke-width="4"/>
115
+
116
+ if(cw){
117
+ svg_g_top += `<use href="#cw_circle" stroke-width="50%" style="color:#fff" x="${M.x}" y="${M.y}" width="2.5%" height="2.5%" transform="translate(-1.25 -1.25)" transform-origin="center" overflow="visible" />
118
+ <use href="#cw_circle" stroke-width="15%" x="${M.x}" y="${M.y}" width="2.5%" height="2.5%" data-transform-origin="center" transform="translate(-1.25 -1.25)" overflow="visible" />`
119
+ }else{
120
+ svg_g_top += `<use href="#ccw_circle" stroke-width="50%" style="color:#fff" x="${M.x}" y="${M.y}" width="2.5%" height="2.5%" transform="translate(-1.25 -1.25)" transform-origin="center" overflow="visible" />
121
+ <use href="#ccw_circle" stroke-width="15%" x="${M.x}" y="${M.y}" width="2.5%" height="2.5%" data-transform-origin="center" transform="translate(-1.25 -1.25)" overflow="visible" />`
122
+ }
123
+ //M = p;
124
+ //renderPoint(svg1, M, 'green', '2%');
125
+
126
+ }
127
+
128
+ if (type === 'C' || type === 'Q') {
129
+ cp1 = { x: values[0], y: values[1] }
130
+ //renderPoint(svg1, cp1, 'cyan');
131
+
132
+ if (type === 'Q') {
133
+ cp2 = cp1
134
+ svg += `<polyline fill="none" class="cpt_handle cpt_handle_q cpt_handle_cp1 " points="${p0.x} ${p0.y} ${cp1.x} ${cp1.y} ${p.x} ${p.y}"/>`;
135
+ svg += `<circle class="cpt_handle_tip cpt_handle_tip_q" cx="${cp1.x}" cy="${cp1.y}" r="1%" >
136
+ <title>${type.toLowerCase()} - x:${cp1.x}, y: ${cp1.y}</title></circle>`
137
+ }
138
+
139
+ if (type === 'C') {
140
+ cp2 = { x: values[2], y: values[3] }
141
+ //renderPoint(svg1, cp2, 'cyan')
142
+
143
+ svg += `<line class="cpt_handle cpt_handle_c cpt_handle_cp1 " x1="${p0.x}" y1="${p0.y}" x2="${cp1.x}" y2="${cp1.y}"/>`;
144
+ svg += `<circle class="cpt_handle_tip" cx="${cp1.x}" cy="${cp1.y}" r="1%" >
145
+ <title>${type.toLowerCase()} - x:${cp1.x}, y: ${cp1.y}</title></circle>`
146
+
147
+
148
+ svg += `<line class="cpt_handle cpt_handle_c cpt_handle_cp2" x1="${p.x}" y1="${p.y}" x2="${cp2.x}" y2="${cp2.y}"/>`;
149
+ svg += `<circle class="cpt_handle_tip" cx="${cp2.x}" cy="${cp2.y}" r="1%" >
150
+ <title>${type.toLowerCase()} - x:${cp2.x}, y: ${cp2.y}</title></circle>`
151
+
152
+ }
153
+ }
154
+
155
+ if (type === 'T') {
156
+
157
+ //get reflected cpt
158
+ // new control points
159
+ cp1 = { x: 2 * p0.x - cp2_0.x, y: 2 * p0.y - cp2_0.y };
160
+ cp2 = cp1
161
+ svg += `<polyline fill="none" class="cpt_handle cpt_handle_t cpt_handle_cp1 " points="${p0.x} ${p0.y} ${cp1.x} ${cp1.y} ${p.x} ${p.y}"/>`;
162
+
163
+ }
164
+
165
+ if (type === 'S') {
166
+
167
+ //get reflected cpt
168
+ let cp2_2 = { x: values[0], y: values[1] }
169
+ // new control points
170
+ let cp1_2 = { x: 2 * p0.x - cp2_0.x, y: 2 * p0.y - cp2_0.y };
171
+
172
+ svg += `<line class="cpt_handle cpt_handle_s cpt_handle_cp1 " x1="${p0.x}" y1="${p0.y}" x2="${cp1_2.x}" y2="${cp1_2.y}"/>`;
173
+ svg += `<line class="cpt_handle cpt_handle_s cpt_handle_cp2" x1="${p.x}" y1="${p.y}" x2="${cp2_2.x}" y2="${cp2_2.y}"/>`;
174
+ svg += `<circle class="cpt_handle_tip cpt_handle_tip_s" cx="${cp2_2.x}" cy="${cp2_2.y}" r="1%" >
175
+ <title>${type.toLowerCase()} - x:${cp2_2.x}, y: ${cp2_2.y}</title></circle>`
176
+
177
+ }
178
+
179
+ if (type !== 'Z') {
180
+ svg += `<circle class="cpt_handle_tip cpt_handle_tip_${type.toLowerCase()}" cx="${p.x}" cy="${p.y}" r="1%" >
181
+ <title>P x:${p.x}, y: ${p.y}</title></circle>`
182
+
183
+ }
184
+
185
+ }
186
+
187
+ p0 = p
188
+ cp1_0 = cp1
189
+ cp2_0 = cp2
190
+
191
+ })
192
+
193
+
194
+ svg_g += svg + '</g>';
195
+
196
+ })
197
+
198
+ svg_g += `</g>`;
199
+ svg_g += svg_g_top+'</g>';
200
+ //console.log('svg', svg, svg1);
201
+ //svg1.insertAdjacentHTML('beforeend', svg)
202
+
203
+ return svg_g
204
+
205
+ }
206
+
207
+
208
+ export function renderPoint(
209
+ svg,
210
+ coords,
211
+ fill = "red",
212
+ r = "1%",
213
+ opacity = "1",
214
+ title = '',
215
+ render = true,
216
+ id = "",
217
+ className = ""
218
+ ) {
219
+ if (Array.isArray(coords)) {
220
+ coords = {
221
+ x: coords[0],
222
+ y: coords[1]
223
+ };
224
+ }
225
+ let marker = `<circle class="${className}" opacity="${opacity}" id="${id}" cx="${coords.x}" cy="${coords.y}" r="${r}" fill="${fill}">
226
+ <title>${title}</title></circle>`;
227
+
228
+ if (render) {
229
+ svg.insertAdjacentHTML("beforeend", marker);
230
+ } else {
231
+ return marker;
232
+ }
233
+ }
234
+
235
+
236
+ export function renderPath(svg, d = '', stroke = 'green', strokeWidth = '1%', render = true) {
237
+
238
+ let path = `<path d="${d}" fill="none" stroke="${stroke}" stroke-width="${strokeWidth}" /> `;
239
+
240
+ if (render) {
241
+ svg.insertAdjacentHTML("beforeend", path);
242
+ } else {
243
+ return path;
244
+ }
245
+
246
+
247
+ }
248
+
249
+
250
+
251
+
252
+ // debug helper: render lines
253
+ export function renderPoly(svg, pts, strokeWidth = "1%", stroke = "purple", render = true) {
254
+ pts = pts.map(pt => { return [pt.x, pt.y] }).flat().join(' ');
255
+
256
+ let poly =
257
+ `<polyline stroke-width="${strokeWidth}" points="${pts}" stroke="${stroke}" />`;
258
+
259
+ if (render) {
260
+ svg.insertAdjacentHTML("beforeend", poly);
261
+
262
+ } else {
263
+ return poly
264
+ }
265
+ }
266
+
267
+
268
+ //!!! delete- rename
269
+ export function renderPerpendicularLine(pt, len = 10, angle) {
270
+ let ptA = {
271
+ x: pt.x + len * Math.cos(angle),
272
+ y: pt.y + len * Math.sin(angle)
273
+ };
274
+ return ptA;
275
+ }
276
+
277
+
278
+
279
+ export function addMarkers() {
280
+
281
+
282
+ let markerMarkup =
283
+ `<svg id="svgMarkers" style="width:0; height:0; position:absolute; z-index:-1;float:left;">
284
+ <defs>
285
+ <marker id="markerStart" overflow="visible" viewBox="0 0 10 10" refX="5" refY="5" markerUnits="strokeWidth"
286
+ markerWidth="10" markerHeight="10" orient="auto-start-reverse">
287
+ <circle cx="5" cy="5" r="3" fill="green" fill-opacity="1" />>
288
+
289
+ <marker id="markerEnd" overflow="visible" viewBox="0 0 10 10" refX="5" refY="5"
290
+ markerUnits="strokeWidth" markerWidth="10" markerHeight="10" orient="auto-start-reverse">
291
+ <circle cx="5" cy="5" r="2" fill="red" fill-opacity="0.5" />
292
+ </marker>
293
+ </defs>
294
+ </svg>`
295
+
296
+ let style = `
297
+ <style>
298
+ .showMarkers {
299
+ marker-start: url(#markerStart);
300
+ marker-mid: url(#markerEnd);
301
+ stroke-width: 0.33%;
302
+ }
303
+ </style>
304
+ `
305
+ document.body.insertAdjacentHTML('afterbegin', style + markerMarkup)
306
+
307
+ }
308
+
309
+
310
+ /**
311
+ * adjust viewBox
312
+ */
313
+ export function adjustViewBox(svg) {
314
+ let bb = svg.getBBox();
315
+ let [x, y, width, height] = [bb.x, bb.y, bb.width, bb.height];
316
+ svg.setAttribute("viewBox", [x, y, width, height].join(" "));
317
+ }
package/LICENSE DELETED
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2025 herrstrietzel
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.