svg-path-simplify 0.4.5 → 0.4.7

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/CHANGELOG.md +14 -0
  2. package/README.md +24 -14
  3. package/dist/svg-path-simplify.esm.js +1620 -1216
  4. package/dist/svg-path-simplify.esm.min.js +2 -2
  5. package/dist/svg-path-simplify.js +1620 -1215
  6. package/dist/svg-path-simplify.min.js +2 -2
  7. package/dist/svg-path-simplify.pathdata.esm.js +298 -665
  8. package/dist/svg-path-simplify.pathdata.esm.min.js +2 -2
  9. package/dist/svg-path-simplify.poly.cjs +8 -9
  10. package/drawing.svg +62 -0
  11. package/index.html +11 -5
  12. package/package.json +1 -1
  13. package/src/index.js +3 -1
  14. package/src/pathData_get_intersections.js +777 -0
  15. package/src/pathData_offset.js +201 -0
  16. package/src/pathData_simplify_cubic.js +14 -34
  17. package/src/pathData_simplify_cubic_extrapolate.js +147 -8
  18. package/src/pathData_simplify_cubicsToArcs.js +65 -21
  19. package/src/pathSimplify-main.js +259 -60
  20. package/src/pathSimplify-presets.js +1 -0
  21. package/src/poly-fit-curve-schneider.js +171 -132
  22. package/src/poly-fit-curve-schneider_check_bulge.js +131 -0
  23. package/src/poly-offset.js +133 -0
  24. package/src/simplify_poly_RC.js +29 -7
  25. package/src/svgii/geometry.js +130 -27
  26. package/src/svgii/geometry_bbox.js +1 -1
  27. package/src/svgii/pathData_analyze.js +6 -28
  28. package/src/svgii/pathData_convert.js +70 -15
  29. package/src/svgii/pathData_remove_collinear.js +32 -36
  30. package/src/svgii/pathData_simplify_refineExtremes.js +1 -3
  31. package/src/svgii/pathData_stringify.js +132 -6
  32. package/src/svgii/pathData_toPolygon.js +33 -23
  33. package/src/svgii/poly_analyze.js +272 -125
  34. package/src/svgii/poly_analyze_cleanup.js +311 -0
  35. package/src/svgii/poly_analyze_getTangents.js +125 -0
  36. package/src/svgii/poly_analyze_get_chunks.js +3 -1
  37. package/src/svgii/poly_normalize.js +12 -0
  38. package/src/svgii/poly_to_pathdata.js +477 -8
  39. package/src/svgii/rounding.js +29 -39
  40. package/src/svgii/svg_cleanup.js +42 -54
  41. package/src/svgii/svg_cleanup_normalize_transforms.js +1 -1
  42. package/src/svgii/visualize.js +33 -2
@@ -5,7 +5,11 @@ import { checkLineIntersection, getAngle, getDeltaAngle, getDeltaAngle2, getDist
5
5
  import { getPolygonArea } from "./geometry_area";
6
6
  import { getPolyBBox } from "./geometry_bbox";
7
7
  import { pathDataFromPoly } from "./pathData_fromPoly";
8
+ import { refineAdjacentExtremes } from "./pathData_simplify_refineExtremes";
8
9
  import { pathDataToD } from "./pathData_stringify";
10
+ import { cleanupPolyKeypoints, refineAdjacentPolyExtremes } from "./poly_analyze_cleanup";
11
+ import { getTangents } from "./poly_analyze_getTangents";
12
+ import { roundPoly } from "./rounding";
9
13
  import { renderPath, renderPoint, renderPoly } from "./visualize";
10
14
 
11
15
 
@@ -19,7 +23,7 @@ export function getPolyCentroid(pts) {
19
23
  y += pt.y
20
24
  }
21
25
 
22
- let centroid = {x: x/l, y:y/l}
26
+ let centroid = { x: x / l, y: y / l }
23
27
  return centroid
24
28
  //console.log(centroid);
25
29
 
@@ -27,20 +31,20 @@ export function getPolyCentroid(pts) {
27
31
 
28
32
  export function getPolyCentroidWeighted(points) {
29
33
  if (!points || points.length === 0) return null;
30
-
34
+
31
35
  let totalWeight = 0;
32
36
  let sumX = 0;
33
37
  let sumY = 0;
34
-
38
+
35
39
  for (const point of points) {
36
40
  let weight = point.weight || 1; // default weight = 1
37
41
  sumX += point.x * weight;
38
42
  sumY += point.y * weight;
39
43
  totalWeight += weight;
40
44
  }
41
-
45
+
42
46
  if (totalWeight === 0) return null;
43
-
47
+
44
48
  return {
45
49
  x: sumX / totalWeight,
46
50
  y: sumY / totalWeight
@@ -49,7 +53,7 @@ export function getPolyCentroidWeighted(points) {
49
53
 
50
54
 
51
55
 
52
- export function detectRegularPolygon(pts, centroid={x:0, y:0}) {
56
+ export function detectRegularPolygon(pts, centroid = { x: 0, y: 0 }) {
53
57
  let rSq = getSquareDistance(pts[0], centroid);
54
58
  let isRegular = true;
55
59
 
@@ -57,8 +61,8 @@ export function detectRegularPolygon(pts, centroid={x:0, y:0}) {
57
61
  let pt1 = pts[i];
58
62
  let dist = getSquareDistance(pt1, centroid);
59
63
 
60
- let diff = Math.abs(rSq-dist);
61
- let diffRel = diff/rSq
64
+ let diff = Math.abs(rSq - dist);
65
+ let diffRel = diff / rSq
62
66
  //console.log('diffRel', diffRel);
63
67
 
64
68
  if (diffRel > 0.05) {
@@ -79,82 +83,237 @@ export function analyzePoly(pts, {
79
83
  debug = false
80
84
  } = {}) {
81
85
 
82
- let l = pts.length;
83
-
84
- // bounding box of this sub poly
85
- let bb0 = getPolyBBox(pts);
86
+ //console.log(width, height);
86
87
 
88
+ let l = pts.length;
89
+ let left = x;
90
+ let top = y;
91
+ let right = x + width;
92
+ let bottom = y + height;
87
93
 
88
94
  if (!width || !height) {
89
- ({ x, y, width, height } = bb0);
95
+ ({ x, y, width, height, top, bottom, left, right } = getPolyBBox(pts));
90
96
  }
91
97
 
98
+ // round
99
+ [x, y, width, height, top, bottom, left, right] = [x, y, width, height, top, bottom, left, right].map(val => +val.toFixed(8))
100
+
101
+
102
+ // bounding box of this sub poly
103
+ let bb0 = { x, y, top, left, width, height, right, bottom }
104
+
105
+
92
106
  //console.log(polyArea);
93
- let thresh = (width + height) * 0.01
107
+ let thresh = (width + height) * 0.01;
108
+
109
+ // threshold for horizontal or vertical detection
110
+ let thresh2 = thresh * 0.75
111
+
112
+ let dims = [];
94
113
 
95
114
  //console.log(thresh);
96
115
 
97
- // get areas an distances
116
+ //pts = roundPoly(pts, 3)
117
+ //console.log(pts);
118
+
119
+ /*
120
+ pts.forEach(pt=>{
121
+ renderPoint(markers, pt, 'red', '2.5%')
122
+ })
123
+ */
124
+
125
+ /**
126
+ * 1st run:
127
+ * collect more details
128
+ * area for sign change detection
129
+ * deltas and distances
130
+ */
98
131
  for (let i = 0; i < l; i++) {
99
132
  let p0 = i > 0 ? pts[i - 1] : pts[l - 1];
100
133
  let p1 = pts[i];
101
134
  let p2 = i < l - 1 ? pts[i + 1] : pts[l - 1];
102
135
 
103
136
  let area = getPolygonArea([p0, p1, p2], false);
137
+ let dx = i > 0 ? +(p1.x - p0.x).toFixed(7) : 0
138
+ let dy = i > 0 ? +(p1.y - p0.y).toFixed(7) : 0
139
+
140
+ let dx2 = +(p2.x - p0.x).toFixed(7)
141
+ let dy2 = +(p2.y - p0.y).toFixed(7)
142
+
143
+
104
144
  p1.area = area;
105
- p1.dist = getDistManhattan(p0, p1)
145
+ p1.dist = i > 0 ? getDistManhattan(p0, p1) : 0;
146
+ // add dist for long/short segment detection
147
+ dims.push(p1.dist);
106
148
  p1.idx = i
149
+ p1.dx = dx
150
+ p1.dy = dy
151
+ p1.dx2 = dx2
152
+ p1.dy2 = dy2
153
+
107
154
  //pts[i] = p1
108
155
  }
109
156
 
110
157
 
111
-
158
+ /**
159
+ * find average segment length
160
+ * for long/short segment detection
161
+ */
162
+ dims = dims.filter(Boolean).sort((a, b) => a - b)
163
+ let lenD = dims.length;
164
+ let dimMin = dims[0];
165
+ let dimMax = dims[lenD - 1];
166
+ //let idxMid = Math.abs(dims.length*0.5);
167
+ let dimAv = dims.reduce((a, b) => a + b, 0) / lenD;
168
+ let dimShort = (dimMin + dimAv) * 0.5
169
+ let dimLong = dimAv * 2;
170
+ //console.log('dims', dims, 'dimAv', dimAv, 'dimMin', dimMin, 'dimMax', dimMax, 'dimShort', dimShort);
171
+ //console.log(pts);
172
+
173
+
174
+ /*
175
+ // round to adjust for minor deviations
176
+ let idx_q = Math.ceil(lenD * 0.25);
177
+ let dim_mid = dims[Math.floor(lenD * 0.5)]
178
+ let dims_min = dims.slice(0, Math.floor(lenD * 0.25));
179
+ let dim_min = ((dims_min.reduce((a, b) => a + b, 0) / idx_q) + dim_mid) * 0.5;
180
+
181
+ let threshold = 75
182
+ let decimalsAuto = dim_min > threshold * 1.5 ? 0 : Math.floor(threshold / dim_min).toString().length
183
+
184
+ // clamp
185
+ decimalsAuto = Math.min(Math.max(0, decimalsAuto), 8)
186
+ //console.log('decimalsAuto', decimalsAuto);
187
+
188
+ pts = roundPoly(pts, 2)
189
+ console.log(pts);
190
+ */
191
+
192
+
193
+ /**
194
+ * analyze topology:
195
+ * find significant commands:
196
+ * extremes, inflections etc.
197
+ */
112
198
  for (let i = 0; i < l; i++) {
113
- let p02 = i > 1 ? pts[i - 2] : pts[l - 1];
199
+ //let p02 = i > 1 ? pts[i - 2] : pts[l - 1];
114
200
  let p0 = i > 0 ? pts[i - 1] : pts[l - 1];
115
201
  let p1 = pts[i];
116
202
  let p2 = i < l - 1 ? pts[i + 1] : pts[l - 1];
117
203
 
118
-
119
- let max = getSquareDistance(p0, p2) * 0.01
120
-
121
- let area0 = Math.abs(p0.area)
204
+ // convert area to absolute for flatness checks
122
205
  let area1 = Math.abs(p1.area)
123
- let area2 = Math.abs(p2.area)
124
- let isCloseExtreme = false
125
206
  let isCorner = false;
207
+ let isSemiExtreme = false;
208
+ let isShort = false;
209
+ let isLong = false;
126
210
 
211
+ /**
212
+ * detect short or long
213
+ */
214
+ if (p1.dist > dimLong) {
215
+ isLong = true;
216
+ }
127
217
 
128
- let flat = !p1.area || area1 < thresh
129
-
130
-
131
- //console.log(bb);
132
- //let extremeLocal = (p1.x < left || p1.x > right || p1.y < top || p1.y > bottom)
218
+ if (p1.dist < dimShort) {
219
+ isShort = true;
220
+ }
133
221
 
134
- let dist = getDistManhattan(p1, p0)
135
- let isNear = dist < thresh * 5
222
+ let flat = !p1.area || area1 < thresh
136
223
 
137
224
 
138
225
 
139
226
  /**
140
227
  * check extremes
141
228
  */
142
-
143
229
  let isExtreme = false;
144
230
 
231
+ //console.log(bb0.top, p1.x, p1.y);
232
+
145
233
  // 1. total extreme
146
- if ((p1.x === bb0.left || p1.x === bb0.right || p1.y === bb0.top || p1.y === bb0.bottom)) {
147
- isExtreme = true
234
+ let isTop = p1.y === bb0.top;
235
+ let isBottom = p1.y === bb0.bottom;
236
+ let isLeft = p1.x === bb0.left;
237
+ let isRight = p1.x === bb0.right;
238
+
239
+ if (i === 0) {
240
+ //console.log(p1, bb0);
241
+ }
242
+
243
+ if (isTop || isBottom || isLeft || isRight) {
244
+ isExtreme = true;
245
+ //renderPoint(markers, p1, 'cyan', '2.75%')
148
246
  }
149
247
 
150
248
  // 1.2 horizontal or vertical
151
- let isHorizontal = p1.y === p0.y && p1.x !== p0.x;
152
- let isVertical = p1.x === p0.x && p1.y !== p0.y
249
+ /*
250
+ let isHorizontal = isTop || isBottom || (p1.y === p0.y && p1.x !== p0.x);
251
+ let isVertical = isLeft || isRight || (p1.x === p0.x && p1.y !== p0.y)
252
+
153
253
 
154
254
  if ((isHorizontal || isVertical)) {
155
- p0.isExtreme = true
255
+
256
+ let diffX = Math.abs(p0.x - p1.x)
257
+ let diffY = Math.abs(p0.y - p1.y)
258
+
259
+ //renderPoint(markers, p0, 'cyan', '2.75%')
260
+
261
+ if (isLong) {
262
+ }
263
+
264
+ if (isLong && (diffY < thresh2) && diffX > thresh) {
265
+ p0.isExtreme = true;
266
+ p0.isHorizontal = true;
267
+ }
268
+ else if (isLong && (diffX < thresh2) && diffY > thresh) {
269
+ p0.isExtreme = true;
270
+ p0.isVertical = true;
271
+ }
272
+
156
273
  isExtreme = true
157
274
  }
275
+ */
276
+
277
+ let dx = Math.abs(p0.x - p1.x)
278
+ let dy = Math.abs(p0.y - p1.y)
279
+
280
+ let vh_thresh = thresh * 0.05
281
+ // vh_thresh = thresh * 0.25
282
+ let isHorizontal = isTop || isBottom || (p1.y === p0.y && p1.x !== p0.x) || (dy <= vh_thresh);
283
+ let isVertical = (isLeft || isRight || (p1.x === p0.x && p1.y !== p0.y) || (dx <= vh_thresh))
284
+
285
+
286
+ // renderPoint(markers, p1, 'red', '0.5%')
287
+
288
+
289
+ if (p1.y === p0.y) {
290
+
291
+ }
292
+
293
+ if (dy > 0) {
294
+ //console.log(p1, dy);
295
+ //renderPoint(markers, p1, 'red', '3%')
296
+ //renderPoint(markers, p0, 'blue', '3%')
297
+ }
298
+
299
+
300
+
301
+ if ((isHorizontal || isVertical)) {
302
+
303
+ //renderPoint(markers, p1, 'red', '3%')
304
+ if (isLong && isHorizontal) {
305
+ p0.isExtreme = true;
306
+ p0.isHorizontal = true;
307
+
308
+ }
309
+ else if (isLong && isVertical) {
310
+ p0.isExtreme = true;
311
+ p0.isVertical = true;
312
+ }
313
+
314
+ isExtreme = true
315
+ }
316
+
158
317
 
159
318
 
160
319
  // 1.3 is local or absolute extreme
@@ -169,12 +328,15 @@ export function analyzePoly(pts, {
169
328
  }
170
329
 
171
330
 
172
-
173
331
  /**
174
332
  * 2. sign changes
175
333
  */
176
334
  let signChange = (p0.area < 0 && p1.area > 0) || (p0.area > 0 && p1.area < 0)
177
- let isDirChange = signChange && !flat && !p0.isDirChange
335
+ let isDirChange = signChange && !flat && !p0.isDirChange && isLong
336
+
337
+ if (isDirChange) {
338
+ //renderPoint(markers, p1, 'green', '3%')
339
+ }
178
340
 
179
341
 
180
342
 
@@ -182,7 +344,6 @@ export function analyzePoly(pts, {
182
344
  * 3. corners
183
345
  */
184
346
 
185
- //isDirChange &&
186
347
  if (isExtreme) {
187
348
 
188
349
  let delta = getDeltaAngle(p1, p2, p0)
@@ -193,125 +354,79 @@ export function analyzePoly(pts, {
193
354
  if (isCornerDelta) {
194
355
  //console.log(deltaAngleDeg);
195
356
  isCorner = true;
357
+ //console.log( p1.dx, p1.dy, p1.dx2, p1.dy2);
196
358
  }
197
359
 
198
360
  }
199
361
 
200
362
 
201
- /*
202
- let debug = true
203
- if (debug) {
204
-
205
- if ((isHorizontal || isVertical)) {
206
- renderPoint(markers, p1, 'blue', '2%', '0.5')
207
- renderPoint(markers, p0, 'blue', '2%', '0.5')
363
+ if (isExtreme && !isCorner) {
364
+ //console.log('dy', p1.dy, p1.dy2);
365
+ if ((Math.abs(p1.dy2) < thresh2) && Math.abs(p1.dx2) > thresh) {
366
+ isHorizontal = true
208
367
  }
209
-
210
- if (isExtreme) {
211
- renderPoint(markers, p1, 'cyan', '1.5%', '0.5')
368
+ else if (Math.abs(p1.dx2) < thresh2 && Math.abs(p1.dy2) > thresh) {
369
+ isVertical = true
212
370
  }
371
+ }
213
372
 
214
- if (isDirChange) {
215
- renderPoint(markers, p1, 'orange', '0.75%', '0.5')
216
- }
217
373
 
218
- if (isCorner) {
219
- renderPoint(markers, p1, 'magenta', '2.75%', '0.5')
220
- }
374
+ /**
375
+ * semi extremes
376
+ * ~ 45deg tangent
377
+ */
378
+ let diffX = Math.abs(p1.dx2)
379
+ let diffY = Math.abs(p1.dy2)
380
+ //let ratDelta = Math.abs(0.5-diffX/diffY)
381
+ let ratDelta = (diffX / diffY)
382
+
383
+ if (ratDelta > 0.8 && ratDelta <= 1.2) {
384
+ isSemiExtreme = true;
221
385
  }
222
- */
223
386
 
224
387
 
225
388
  p1.isCorner = isCorner;
226
389
  p1.isExtreme = isExtreme;
390
+ p1.isSemiExtreme = isSemiExtreme;
391
+ p1.isLong = isLong;
392
+ p1.isShort = isShort;
393
+
227
394
  p1.isHorizontal = isHorizontal;
228
395
  p1.isVertical = isVertical;
229
396
  p1.isDirChange = isDirChange;
230
397
 
231
398
  //renderPoint(markers, p1, 'red', '2%', '0.5')
232
399
 
233
-
234
400
  }
235
401
 
236
402
 
237
- // filter adjacent extremes
238
- let pts1 = []
239
- let exclude = []
240
- let filterExtremes = true;
241
-
242
- if (filterExtremes) {
243
- for (let i = 0; i < pts.length; i++) {
244
- let p = pts[i]
245
- let p1 = pts[i + 1] || null
246
- let p2 = pts[i + 2] || null
247
-
248
- let extremes = []
249
-
250
- if (p1 && p1.isExtreme && p.isExtreme && !p.isCorner) {
251
- let has2nd = p1.dist < thresh * 2 && !p1.isCorner
252
- let has3rd = p2 && p2.isExtreme && p2.dist < thresh * 2 && !p2.isCorner
253
- let lastExt = p1
254
-
255
- if (has2nd && !has3rd) {
256
- extremes.push(p, p1)
257
- //renderPoint(markers, p1, 'magenta', '1%', '0.5')
258
- } else if (has3rd) {
259
- extremes.push(p, p1, p2)
260
- /*
261
- renderPoint(markers, p, 'green', '1%', '0.5')
262
- renderPoint(markers, p1, 'red', '1%', '0.5')
263
- renderPoint(markers, p2, 'blue', '1%', '0.5')
264
- */
265
- }
266
-
267
- if (extremes.length) {
268
- // average extreme
269
- //console.log(extremes);
270
- let x = extremes.reduce((a, b) => a + b.x, 0) / extremes.length
271
- let y = extremes.reduce((a, b) => a + b.y, 0) / extremes.length
272
-
273
- ///extremes.length
274
- p.x = x
275
- p.y = y
276
- //console.log(x);
277
- i += extremes.length - 1
278
- }
279
- }
403
+ // add tangents
404
+ getTangents(pts, { x, y, width, height })
280
405
 
281
- if (p.isExtreme || p.isCorner || p.isDirChange) {
282
- exclude.push(pts1.length)
283
- }
406
+ refineAdjacentPolyExtremes(pts)
284
407
 
408
+ // filter adjacent significant points
409
+ cleanupPolyKeypoints(pts);
285
410
 
286
- pts1.push(p)
287
-
288
- // remove last nearby extreme
289
- let l2 = pts1.length;
290
- let p0 = pts1[0]
291
- let pL = pts1[l2 - 1]
292
- let near0 = getDistManhattan(p0, pL) < thresh * 2
293
- if (p0.isExtreme && pL.isExtreme && near0) {
294
- pL.x = p0.x
295
- pL.y = p0.y
296
- }
297
- }
298
-
299
- pts = pts1
300
- }
301
-
411
+ renderPolyTopology(pts)
302
412
 
303
413
  return pts
304
414
  }
305
415
 
416
+ /*
306
417
 
307
-
418
+ */
308
419
 
309
420
 
310
421
 
311
422
  // just for visualization
312
- function renderPolyTopology(pts) {
423
+ function renderPolyTopology(pts, showTangents = true) {
313
424
 
314
425
  let l = pts.length
426
+
427
+
428
+ //renderPoint(markers, pts[0], 'green', '1.5%', '0.5')
429
+
315
430
  // render
316
431
  for (let i = 0; i < l; i++) {
317
432
  let p0 = i > 0 ? pts[i - 1] : pts[l - 1];
@@ -319,24 +434,38 @@ function renderPolyTopology(pts) {
319
434
  let p2 = i < l - 1 ? pts[i + 1] : pts[l - 1];
320
435
 
321
436
 
322
- renderPoly(markers, [p0, p1, p2], '0.5%', 'none', (p1.area > 0 ? 'green' : 'blue'), true)
437
+ //renderPoly(markers, [p0, p1, p2], '0.5%', 'none', (p1.area > 0 ? 'green' : 'blue'), true)
323
438
 
324
439
 
325
440
  if (p1.isDirChange) {
326
441
  renderPoint(markers, p1, 'orange', '1%', '0.75')
327
442
  }
328
443
 
444
+ if (p1.isSemiExtreme) {
445
+ renderPoint(markers, p1, 'red', '1%', '0.5')
446
+ }
447
+
448
+ /*
449
+ if (p1.isLong && (p1.isDirChange || p1.isExtreme || p1.isCorner || p1.isSemiExtreme)) {
450
+ renderPoint(markers, p1, 'green', '1.5%', '0.5')
451
+ }
452
+ */
453
+
454
+ if (p1.isDirChange) {
455
+ renderPoint(markers, p1, 'green', '1.5%', '0.5')
456
+ }
457
+
329
458
 
330
459
  if (p1.isExtreme) {
331
460
  renderPoint(markers, p1, 'cyan', '1%', '0.5')
332
461
  }
333
462
 
334
463
  if (p1.isHorizontal) {
335
- //renderPoint(markers, p1, 'blue', '0.5%', '0.75')
464
+ renderPoint(markers, p1, 'blue', '1.5%', '0.25')
336
465
  }
337
466
 
338
467
  if (p1.isVertical) {
339
- //renderPoint(markers, p1, 'purple', '0.5%', '0.5')
468
+ renderPoint(markers, p1, 'purple', '1.5%', '0.25')
340
469
  }
341
470
 
342
471
 
@@ -345,8 +474,26 @@ function renderPolyTopology(pts) {
345
474
  }
346
475
 
347
476
  //pts[i] = p1
477
+
478
+ if (showTangents && (p1.isCorner || p1.isSemiExtreme || p1.isDirChange || p1.isExtreme)) {
479
+ renderPoint(markers, p1.tangentL, 'darkred', '0.5%')
480
+ renderPoint(markers, p1.tangentR, 'darkblue', '0.5%')
481
+
482
+ /*
483
+ if (p1.isDirChange) {
484
+ renderPoint(markers, p1.tangentL, 'darkred', '1.5%')
485
+ renderPoint(markers, p1.tangentR, 'darkblue', '1.5%')
486
+ }
487
+ */
488
+
489
+
490
+
491
+ }
492
+
348
493
  }
349
494
 
495
+
496
+
350
497
  }
351
498
 
352
499