• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

miyanokomiya / okageo / 3973046296

pending completion
3973046296

push

github

miyanokomiya
fix: Unexpected error for parsing arc segment when its radius is close to 0

451 of 543 branches covered (83.06%)

Branch coverage included in aggregate %.

3 of 3 new or added lines in 2 files covered. (100.0%)

1479 of 1566 relevant lines covered (94.44%)

42.89 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

89.35
/src/svg.ts
1
import { AffineMatrix, ISvgConfigs, ISvgPath, ISvgStyle, IVec2 } from './types'
2
import * as geo from './geo'
1✔
3

4
const HTTP_SVG = 'http://www.w3.org/2000/svg'
1✔
5
// Unary plus operator seems faster than native parseFloat
6
const _parseFloat = (v: string) => +v
456✔
7

8
export const configs: ISvgConfigs = {
1✔
9
  bezierSplitSize: 10,
10
  ellipseSplitSize: 20,
11
}
12

13
/**
14
 * 描画
15
 * @param ctx 描画要素
16
 * @param pathInfo 図形情報
17
 */
18
export function draw(ctx: CanvasRenderingContext2D, pathInfo: ISvgPath): void {
1✔
19
  ctx.lineCap = pathInfo.style.lineCap as any
×
20
  ctx.lineJoin = pathInfo.style.lineJoin as any
×
21

22
  ctx.beginPath()
×
23
  pathInfo.d.forEach((p, i) => {
×
24
    if (i === 0) {
×
25
      ctx.moveTo(p.x, p.y)
×
26
    } else {
27
      ctx.lineTo(p.x, p.y)
×
28
    }
29
  })
30
  ctx.closePath()
×
31

32
  if (pathInfo.included) {
×
33
    pathInfo.included.forEach((poly) => {
×
34
      poly.forEach((p, i) => {
×
35
        if (i === 0) {
×
36
          ctx.moveTo(p.x, p.y)
×
37
        } else {
38
          ctx.lineTo(p.x, p.y)
×
39
        }
40
      })
41
      ctx.closePath()
×
42
    })
43
  }
44

45
  if (pathInfo.style.fill) {
×
46
    ctx.fillStyle = pathInfo.style.fillStyle
×
47
    ctx.globalAlpha = pathInfo.style.fillGlobalAlpha
×
48
    ctx.fill()
×
49
  }
50

51
  // 枠
52
  if (pathInfo.style.stroke) {
×
53
    ctx.strokeStyle = pathInfo.style.strokeStyle
×
54
    ctx.globalAlpha = pathInfo.style.strokeGlobalAlpha
×
55
    ctx.lineWidth = pathInfo.style.lineWidth
×
56
    ctx.setLineDash(pathInfo.style.lineDash)
×
57
    ctx.stroke()
×
58
  }
59
  ctx.globalAlpha = 1
×
60
}
61

62
/**
63
 * 矩形に収まるよう調整
64
 * @param pathInfoList パス情報リスト
65
 * @param x 矩形x座標
66
 * @param y 矩形y座標
67
 * @param width 矩形width
68
 * @param height 矩形height
69
 * @return 調整後パス情報リスト
70
 */
71
export function fitRect(
1✔
72
  pathInfoList: ISvgPath[],
73
  x: number,
74
  y: number,
75
  width: number,
76
  height: number
77
): ISvgPath[] {
78
  let minX: number = Infinity
4✔
79
  let maxX: number = -Infinity
4✔
80
  let minY: number = Infinity
4✔
81
  let maxY: number = -Infinity
4✔
82
  pathInfoList.forEach((info) => {
4✔
83
    info.d.forEach((p) => {
4✔
84
      minX = Math.min(minX, p.x)
8✔
85
      maxX = Math.max(maxX, p.x)
8✔
86
      minY = Math.min(minY, p.y)
8✔
87
      maxY = Math.max(maxY, p.y)
8✔
88
    })
89
  })
90

91
  // 原点基準に移動
92
  const fromBaseList = pathInfoList.map((info) => ({
4✔
93
    ...info,
94
    d: info.d.map((p) => geo.vec(p.x - minX, p.y - minY)),
8✔
95
  }))
96
  // 伸縮
97
  const orgWidth = maxX - minX
4✔
98
  const orgHeight = maxY - minY
4✔
99
  const rateX = width / orgWidth
4✔
100
  const rateY = height / orgHeight
4✔
101
  const rate = Math.min(rateX, rateY)
4✔
102
  const scaledList = fromBaseList.map((info) => ({
4✔
103
    ...info,
104
    d: info.d.map((p) => geo.vec(p.x * rate, p.y * rate)),
8✔
105
  }))
106
  // 矩形位置に移動
107
  const difX = x + (width - orgWidth * rate) / 2
4✔
108
  const difY = y + (height - orgHeight * rate) / 2
4✔
109
  const convertedList: ISvgPath[] = scaledList.map((info) => ({
4✔
110
    ...info,
111
    d: info.d.map((p) => geo.vec(p.x + difX, p.y + difY)),
8✔
112
    included: (info.included || []).map((poly: IVec2[]) => {
8✔
113
      return poly.map((p) =>
×
114
        geo.vec((p.x - minX) * rate + difX, (p.y - minY) * rate + difY)
×
115
      )
116
    }),
117
  }))
118

119
  return convertedList
4✔
120
}
121

122
/**
123
 * SVG文字列から図形のパス情報を取得する
124
 * 対応タグ: path,rect,ellipse,circle
125
 * @param svgString SVGリソース文字列
126
 * @return パス情報リスト
127
 */
128
export function parseSvgGraphicsStr(svgString: string): ISvgPath[] {
1✔
129
  const domParser = new DOMParser()
1✔
130
  const svgDom = domParser.parseFromString(svgString, 'image/svg+xml')
1✔
131
  const svgTags = svgDom.getElementsByTagName('svg')
1✔
132
  if (!svgTags || svgTags.length === 0) return []
1!
133
  return parseSvgGraphics(svgTags[0] as SVGElement)
1✔
134
}
135

136
/**
137
 * parse SVG tree
138
 * @param elm SVGElement
139
 * @return path informations
140
 */
141
function parseSvgTree(
142
  elm: SVGElement,
143
  parentInfo?: { style?: ISvgStyle; transform?: AffineMatrix }
144
): ISvgPath[] {
145
  const style = { ...(parentInfo?.style ?? {}), ...parseTagStyle(elm) }
18✔
146

147
  const transformStr = elm.getAttribute('transform')
18✔
148
  const parentTransform = parentInfo?.transform ?? geo.IDENTITY_AFFINE
18✔
149

150
  let ret: ISvgPath[] = []
18✔
151

152
  const svgPath = parseSVGShape(elm)
18✔
153
  if (svgPath) {
18✔
154
    ret.push({
8✔
155
      ...svgPath,
156
      d: svgPath.d.map((v) => geo.applyAffine(parentTransform, v)),
81✔
157
    })
158
  }
159

160
  if (elm.children.length > 0) {
18✔
161
    const transform = transformStr
10✔
162
      ? geo.multiAffine(parentTransform, parseTransform(transformStr))
10✔
163
      : parentTransform
164

165
    Array.from(elm.children).forEach((child) => {
10✔
166
      ret = ret.concat(parseSvgTree(child as SVGElement, { style, transform }))
11✔
167
    })
168
  }
169

170
  return ret
18✔
171
}
172

173
function parseSVGShape(elm: SVGElement): ISvgPath | undefined {
174
  switch (elm.tagName.toLowerCase()) {
18✔
175
    case 'path':
18✔
176
      return {
2✔
177
        d: parsePath(elm as SVGPathElement),
178
        style: parseTagStyle(elm),
179
      }
180
    case 'rect':
181
      return {
3✔
182
        d: parseRect(elm as SVGRectElement),
183
        style: parseTagStyle(elm),
184
      }
185
    case 'ellipse':
186
      return {
1✔
187
        d: parseEllipse(elm as SVGEllipseElement),
188
        style: parseTagStyle(elm),
189
      }
190
    case 'circle':
191
      return {
2✔
192
        d: parseCircle(elm as SVGCircleElement),
193
        style: parseTagStyle(elm),
194
      }
195
    default:
196
      return undefined
10✔
197
  }
198
}
199

200
/**
201
 * SVGタグから図形のパス情報を取得する
202
 * 対応タグ: path,rect,ellipse,circle
203
 * @param svgTag SVGタグ
204
 * @return パス情報リスト
205
 */
206
export function parseSvgGraphics(svgTag: SVGElement): ISvgPath[] {
1✔
207
  return parseSvgTree(svgTag)
7✔
208
}
209

210
/**
211
 * opentype.jsのpath.commandをd文字列に変換する
212
 * @param fontPath opentype.jsのpath.command
213
 * @return d文字列
214
 */
215
export function openCommandToD(command: any): string {
1✔
216
  let d: string = command.type
7✔
217
  if ('x1' in command) d += ` ${command.x1}`
7✔
218
  if ('y1' in command) d += ` ${command.y1}`
7✔
219
  if ('x2' in command) d += ` ${command.x2}`
7✔
220
  if ('y2' in command) d += ` ${command.y2}`
7✔
221
  if ('x3' in command) d += ` ${command.x3}`
7!
222
  if ('y3' in command) d += ` ${command.y3}`
7!
223
  if ('x' in command) d += ` ${command.x}`
7✔
224
  if ('y' in command) d += ` ${command.y}`
7✔
225
  return d
7✔
226
}
227

228
/**
229
 * opentype.jsのpathを解析する
230
 * @param fontPath opentype.jsのpath
231
 * @return パス情報リスト
232
 */
233
export function parseOpenPath(fontPath: { commands: any[] }): ISvgPath[] {
1✔
234
  const pathInfoList: ISvgPath[] = []
1✔
235
  let current: string = ''
1✔
236
  fontPath.commands.forEach((c: any) => {
1✔
237
    current += openCommandToD(c) + ' '
4✔
238
    if (current && c.type.toUpperCase() === 'Z') {
4✔
239
      const pathList = parsePathD(current)
1✔
240
      pathInfoList.push({
1✔
241
        d: pathList,
242
        style: {
243
          ...createStyle(),
244
          fill: true,
245
          fillStyle: 'black',
246
          stroke: false,
247
        },
248
      })
249
      current = ''
1✔
250
    }
251
  })
252
  return pathInfoList
1✔
253
}
254

255
export type PathSegmentRaw =
256
  | ['Z' | 'z']
257
  | ['H' | 'h' | 'V' | 'v', number]
258
  | ['M' | 'm' | 'L' | 'l' | 'T' | 't', number, number]
259
  | ['Q' | 'q' | 'S' | 's', number, number, number, number]
260
  | ['C' | 'c', number, number, number, number, number, number]
261
  | ['A' | 'a', number, number, number, boolean, boolean, number, number]
262

263
function parsePathSegmentRaw(segment: string[]): PathSegmentRaw {
264
  if (segment.length === 8) {
148✔
265
    return [
5✔
266
      segment[0],
267
      _parseFloat(segment[1]),
268
      _parseFloat(segment[2]),
269
      _parseFloat(segment[3]),
270
      segment[4] !== '0',
271
      segment[5] !== '0',
272
      _parseFloat(segment[6]),
273
      _parseFloat(segment[7]),
274
    ] as PathSegmentRaw
275
  } else {
276
    const [c, ...values] = segment
143✔
277
    return [c, ...values.map(_parseFloat)] as PathSegmentRaw
143✔
278
  }
279
}
280

281
export function parsePathSegmentRaws(dStr: string): PathSegmentRaw[] {
1✔
282
  return splitD(dStr).map((c) => parsePathSegmentRaw(c))
148✔
283
}
284

285
export function pathSegmentRawsToString(segs: PathSegmentRaw[]): string {
1✔
286
  return segs.map(pathSegmentRawToString).join(' ')
×
287
}
288

289
export function pathSegmentRawToString(seg: PathSegmentRaw): string {
1✔
290
  return seg
×
291
    .map((v) => {
292
      if (v === true) {
×
293
        return '1'
×
294
      } else if (v === false) {
×
295
        return '0'
×
296
      } else {
297
        return v.toString()
×
298
      }
299
    })
300
    .join(' ')
301
}
302

303
type PathSegment =
304
  | {
305
      command: string
306
      lerpFn: (t: number) => IVec2
307
      curve: true
308
    }
309
  | {
310
      command: string
311
      segment: [IVec2, IVec2]
312
      curve?: undefined
313
    }
314

315
export function parsePathSegments(dStr: string): PathSegment[] {
1✔
316
  return _parsePathSegments(parsePathSegmentRaws(dStr))
39✔
317
}
318

319
function _parsePathSegments(segments: PathSegmentRaw[]): PathSegment[] {
320
  const ret: PathSegment[] = []
39✔
321
  let startP = geo.vec(0, 0)
39✔
322
  let currentP = startP
39✔
323
  let currentControlP = startP
39✔
324
  let currentBezier: 1 | 2 | 3 = 1
39✔
325
  segments.forEach((current) => {
39✔
326
    switch (current[0]) {
148✔
327
      case 'M': {
149✔
328
        const p1 = geo.vec(current[1], current[2])
56✔
329
        ret.push({ command: 'M', segment: [p1, p1] })
56✔
330
        startP = p1
56✔
331
        currentControlP = p1
56✔
332
        currentBezier = 1
56✔
333
        currentP = p1
56✔
334
        break
56✔
335
      }
336
      case 'm': {
337
        const p1 = geo.vec(current[1], current[2])
1✔
338
        ret.push({ command: 'm', segment: [p1, p1] })
1✔
339
        startP = p1
1✔
340
        currentP = p1
1✔
341
        currentControlP = p1
1✔
342
        currentBezier = 1
1✔
343
        break
1✔
344
      }
345
      case 'L': {
346
        const p0 = currentP
45✔
347
        const p1 = geo.vec(current[1], current[2])
45✔
348
        ret.push({ command: 'L', segment: [p0, p1] })
45✔
349
        startP ??= p1
45!
350
        currentControlP = p1
45✔
351
        currentBezier = 1
45✔
352
        currentP = p1
45✔
353
        break
45✔
354
      }
355
      case 'l': {
356
        const p0 = currentP
2✔
357
        const p1 = geo.add(currentP, geo.vec(current[1], current[2]))
2✔
358
        ret.push({ command: 'l', segment: [p0, p1] })
2✔
359
        startP ??= p1
2!
360
        currentControlP = p1
2✔
361
        currentBezier = 1
2✔
362
        currentP = p1
2✔
363
        break
2✔
364
      }
365
      case 'H': {
366
        const p0 = currentP
1✔
367
        const p1 = geo.vec(current[1], p0.y)
1✔
368
        ret.push({ command: 'H', segment: [p0, p1] })
1✔
369
        currentControlP = p1
1✔
370
        currentBezier = 1
1✔
371
        currentP = p1
1✔
372
        break
1✔
373
      }
374
      case 'h': {
375
        const p0 = currentP
1✔
376
        const p1 = geo.vec(current[1] + p0.x, p0.y)
1✔
377
        ret.push({ command: 'h', segment: [p0, p1] })
1✔
378
        currentControlP = p1
1✔
379
        currentBezier = 1
1✔
380
        currentP = p1
1✔
381
        break
1✔
382
      }
383
      case 'V': {
384
        const p0 = currentP
1✔
385
        const p1 = geo.vec(p0.x, current[1])
1✔
386
        ret.push({ command: 'V', segment: [p0, p1] })
1✔
387
        currentControlP = p1
1✔
388
        currentBezier = 1
1✔
389
        currentP = p1
1✔
390
        break
1✔
391
      }
392
      case 'v': {
393
        const p0 = currentP
1✔
394
        const p1 = geo.vec(p0.x, current[1] + p0.y)
1✔
395
        ret.push({ command: 'v', segment: [p0, p1] })
1✔
396
        currentControlP = p1
1✔
397
        currentBezier = 1
1✔
398
        currentP = p1
1✔
399
        break
1✔
400
      }
401
      case 'Q': {
402
        const p0 = currentP
8✔
403
        const p1 = geo.vec(current[1], current[2])
8✔
404
        const p2 = geo.vec(current[3], current[4])
8✔
405
        ret.push({
8✔
406
          command: 'Q',
407
          lerpFn: geo.getBezier2LerpFn([p0, p1, p2]),
408
          curve: true,
409
        })
410
        currentControlP = p1
8✔
411
        currentBezier = 2
8✔
412
        currentP = p2
8✔
413
        break
8✔
414
      }
415
      case 'q': {
416
        const p0 = currentP
1✔
417
        const p1 = geo.add(p0, geo.vec(current[1], current[2]))
1✔
418
        const p2 = geo.add(p0, geo.vec(current[3], current[4]))
1✔
419
        ret.push({
1✔
420
          command: 'q',
421
          lerpFn: geo.getBezier2LerpFn([p0, p1, p2]),
422
          curve: true,
423
        })
424
        currentControlP = p1
1✔
425
        currentBezier = 2
1✔
426
        currentP = p2
1✔
427
        break
1✔
428
      }
429
      case 'T': {
430
        const p0 = currentP
2✔
431
        const p1 =
432
          currentBezier === 2 ? geo.getSymmetry(currentControlP, p0) : p0
2✔
433
        const p2 = geo.vec(current[1], current[2])
2✔
434
        ret.push({
2✔
435
          command: 'T',
436
          lerpFn: geo.getBezier2LerpFn([p0, p1, p2]),
437
          curve: true,
438
        })
439
        currentControlP = p1
2✔
440
        currentBezier = 2
2✔
441
        currentP = p2
2✔
442
        break
2✔
443
      }
444
      case 't': {
445
        const p0 = currentP
2✔
446
        const p1 =
447
          currentBezier === 2 ? geo.getSymmetry(currentControlP, p0) : p0
2✔
448
        const p2 = geo.add(p0, geo.vec(current[1], current[2]))
2✔
449
        ret.push({
2✔
450
          command: 't',
451
          lerpFn: geo.getBezier2LerpFn([p0, p1, p2]),
452
          curve: true,
453
        })
454
        currentControlP = p1
2✔
455
        currentBezier = 2
2✔
456
        currentP = p2
2✔
457
        break
2✔
458
      }
459
      case 'C': {
460
        const p0 = currentP
5✔
461
        const p1 = geo.vec(current[1], current[2])
5✔
462
        const p2 = geo.vec(current[3], current[4])
5✔
463
        const p3 = geo.vec(current[5], current[6])
5✔
464
        ret.push({
5✔
465
          command: 'C',
466
          lerpFn: geo.getBezier3LerpFn([p0, p1, p2, p3]),
467
          curve: true,
468
        })
469
        currentControlP = p2
5✔
470
        currentBezier = 3
5✔
471
        currentP = p3
5✔
472
        break
5✔
473
      }
474
      case 'c': {
475
        const p0 = currentP
1✔
476
        const p1 = geo.add(p0, geo.vec(current[1], current[2]))
1✔
477
        const p2 = geo.add(p0, geo.vec(current[3], current[4]))
1✔
478
        const p3 = geo.add(p0, geo.vec(current[5], current[6]))
1✔
479
        ret.push({
1✔
480
          command: 'c',
481
          lerpFn: geo.getBezier3LerpFn([p0, p1, p2, p3]),
482
          curve: true,
483
        })
484
        currentControlP = p2
1✔
485
        currentBezier = 3
1✔
486
        currentP = p3
1✔
487
        break
1✔
488
      }
489
      case 'S': {
490
        const p0 = currentP
2✔
491
        const p1 =
492
          currentBezier === 3 ? geo.getSymmetry(currentControlP, p0) : p0
2✔
493
        const p2 = geo.vec(current[1], current[2])
2✔
494
        const p3 = geo.vec(current[3], current[4])
2✔
495
        ret.push({
2✔
496
          command: 'S',
497
          lerpFn: geo.getBezier3LerpFn([p0, p1, p2, p3]),
498
          curve: true,
499
        })
500
        currentControlP = p2
2✔
501
        currentBezier = 3
2✔
502
        currentP = p3
2✔
503
        break
2✔
504
      }
505
      case 's': {
506
        const p0 = currentP
2✔
507
        const p1 =
508
          currentBezier === 3 ? geo.getSymmetry(currentControlP, p0) : p0
2✔
509
        const p2 = geo.add(p0, geo.vec(current[1], current[2]))
2✔
510
        const p3 = geo.add(p0, geo.vec(current[3], current[4]))
2✔
511
        ret.push({
2✔
512
          command: 's',
513
          lerpFn: geo.getBezier3LerpFn([p0, p1, p2, p3]),
514
          curve: true,
515
        })
516
        currentControlP = p2
2✔
517
        currentBezier = 3
2✔
518
        currentP = p3
2✔
519
        break
2✔
520
      }
521
      case 'A': {
522
        const p0 = currentP
4✔
523
        const rx = current[1]
4✔
524
        const ry = current[2]
4✔
525
        const large = current[4]
4✔
526
        const sweep = current[5]
4✔
527
        const radian = (current[3] / 180) * Math.PI
4✔
528
        const p1 = geo.vec(current[6], current[7])
4✔
529
        ret.push({
4✔
530
          command: 'A',
531
          lerpFn: geo.getArcLerpFn(rx, ry, p0, p1, large, sweep, radian),
532
          curve: true,
533
        })
534
        currentControlP = p1
4✔
535
        currentBezier = 1
4✔
536
        currentP = p1
4✔
537
        break
4✔
538
      }
539
      case 'a': {
540
        const p0 = currentP
1✔
541
        const rx = current[1]
1✔
542
        const ry = current[2]
1✔
543
        const large = current[4]
1✔
544
        const sweep = current[5]
1✔
545
        const radian = (current[3] / 180) * Math.PI
1✔
546
        const p1 = geo.add(p0, geo.vec(current[6], current[7]))
1✔
547
        ret.push({
1✔
548
          command: 'a',
549
          lerpFn: geo.getArcLerpFn(rx, ry, p0, p1, large, sweep, radian),
550
          curve: true,
551
        })
552
        currentControlP = p1
1✔
553
        currentBezier = 1
1✔
554
        currentP = p1
1✔
555
        break
1✔
556
      }
557
      case 'Z':
558
      case 'z': {
559
        const p0 = currentP
12✔
560
        const p1 = startP
12✔
561
        ret.push({
12✔
562
          command: current[0],
563
          segment: [p0, p1],
564
        })
565
        currentControlP = p1
12✔
566
        currentBezier = 1
12✔
567
        currentP = p1
12✔
568
        break
12✔
569
      }
570
    }
571
  })
572

573
  return ret
39✔
574
}
575

576
export interface PathLengthStruct {
577
  lerpFn: (t: number) => IVec2
578
  length: number
579
}
580

581
export function getPathLengthStructs(
1✔
582
  dStr: string,
583
  split = configs.bezierSplitSize
1✔
584
): PathLengthStruct[] {
585
  return parsePathSegments(dStr).map((seg) => ({
79✔
586
    lerpFn: seg.curve
587
      ? seg.lerpFn
79✔
588
      : (t) => geo.lerpPoint(seg.segment[0], seg.segment[1], t),
7✔
589
    length: geo.getPolylineLength(
590
      seg.curve ? geo.getApproPoints(seg.lerpFn, split) : seg.segment
79✔
591
    ),
592
  }))
593
}
594

595
/**
596
 * Execute "getPathTotalLength" with cacheable structs generated by "getPathLengthStructs"
597
 */
598
export function getPathTotalLengthFromStructs(
1✔
599
  structs: PathLengthStruct[]
600
): number {
601
  return structs.reduce((p, s) => p + s.length, 0)
10✔
602
}
603

604
/**
605
 * Alternative function of "SVGGeometryElement.getTotalLength"
606
 * @param dStr d string of path element
607
 * @param split the number of segments to approximate a curve
608
 * @return total length of the path
609
 */
610
export function getPathTotalLength(
1✔
611
  dStr: string,
612
  split = configs.bezierSplitSize
2✔
613
): number {
614
  return getPathTotalLengthFromStructs(getPathLengthStructs(dStr, split))
2✔
615
}
616

617
/**
618
 * Execute "getPathPointAtLength" with cacheable structs generated by "getPathLengthStructs"
619
 */
620
export function getPathPointAtLengthFromStructs(
1✔
621
  structs: PathLengthStruct[],
622
  distance: number
623
): IVec2 {
624
  let l = Math.max(distance, 0)
9✔
625
  for (let i = 0; i < structs.length; i++) {
9✔
626
    const s = structs[i]
34✔
627
    if (l < s.length) {
34✔
628
      return s.lerpFn(l / s.length)
6✔
629
    } else {
630
      l -= s.length
28✔
631
    }
632
  }
633
  return structs.length > 0
3✔
634
    ? structs[structs.length - 1].lerpFn(1)
3!
635
    : geo.vec(0, 0)
636
}
637

638
/**
639
 * Alternative function of "SVGGeometryElement.getPointAtLength"
640
 * @param dStr d string of path element
641
 * @param distance target length
642
 * @param split the number of segments to approximate a curve
643
 * @return the point at the target length
644
 */
645
export function getPathPointAtLength(
1✔
646
  dStr: string,
647
  distance: number,
648
  split = configs.bezierSplitSize
9✔
649
): IVec2 {
650
  return getPathPointAtLengthFromStructs(
9✔
651
    getPathLengthStructs(dStr, split),
652
    distance
653
  )
654
}
655

656
function getPathAbsPoints(segments: PathSegmentRaw[]): {
657
  controls: IVec2[]
658
  points: IVec2[]
659
} {
660
  const points: IVec2[] = []
24✔
661
  const controls: IVec2[] = []
24✔
662

663
  let seg: PathSegmentRaw
664
  let startP = geo.vec(0, 0)
24✔
665
  let absP = startP
24✔
666
  let preC = startP
24✔
667
  let preCType: 1 | 2 | 3 = 1
24✔
668
  for (let i = 0; i < segments.length; i++) {
24✔
669
    seg = segments[i]
65✔
670
    switch (seg[0]) {
65✔
671
      case 'M': {
67!
672
        const p = geo.vec(seg[1], seg[2])
19✔
673
        startP = absP = preC = p
19✔
674
        preCType = 1
19✔
675
        break
19✔
676
      }
677
      case 'm': {
678
        const p = geo.add(geo.vec(seg[1], seg[2]), absP)
1✔
679
        startP = absP = preC = p
1✔
680
        preCType = 1
1✔
681
        break
1✔
682
      }
683
      case 'L': {
684
        const p = geo.vec(seg[1], seg[2])
12✔
685
        startP ??= p
12!
686
        absP = preC = p
12✔
687
        preCType = 1
12✔
688
        break
12✔
689
      }
690
      case 'l': {
691
        const p = geo.add(geo.vec(seg[1], seg[2]), absP)
5✔
692
        startP ??= p
5!
693
        absP = preC = p
5✔
694
        preCType = 1
5✔
695
        break
5✔
696
      }
697
      case 'H': {
698
        const p = geo.vec(seg[1], absP.y)
2✔
699
        absP = preC = p
2✔
700
        preCType = 1
2✔
701
        break
2✔
702
      }
703
      case 'h': {
704
        const p = geo.vec(seg[1] + absP.x, absP.y)
2✔
705
        absP = preC = p
2✔
706
        preCType = 1
2✔
707
        break
2✔
708
      }
709
      case 'V': {
710
        const p = geo.vec(absP.x, seg[1])
2✔
711
        absP = preC = p
2✔
712
        preCType = 1
2✔
713
        break
2✔
714
      }
715
      case 'v': {
716
        const p = geo.vec(absP.x, seg[1] + absP.y)
2✔
717
        absP = preC = p
2✔
718
        preCType = 1
2✔
719
        break
2✔
720
      }
721
      case 'Q': {
722
        const p = geo.vec(seg[1], seg[2])
5✔
723
        preC = p
5✔
724
        absP = geo.vec(seg[3], seg[4])
5✔
725
        preCType = 2
5✔
726
        break
5✔
727
      }
728
      case 'q': {
729
        const p = geo.vec(seg[1] + absP.x, seg[2] + absP.y)
1✔
730
        preC = p
1✔
731
        absP = geo.vec(seg[3] + absP.x, seg[4] + absP.y)
1✔
732
        preCType = 2
1✔
733
        break
1✔
734
      }
735
      case 'T': {
736
        const p = preCType === 2 ? geo.lerpPoint(preC, absP, 2) : absP
1!
737
        preC = p
1✔
738
        absP = geo.vec(seg[1], seg[2])
1✔
739
        preCType = 2
1✔
740
        break
1✔
741
      }
742
      case 't': {
743
        const p = preCType === 2 ? geo.lerpPoint(preC, absP, 2) : absP
1!
744
        preC = p
1✔
745
        absP = geo.vec(seg[1] + absP.x, seg[2] + absP.y)
1✔
746
        preCType = 2
1✔
747
        break
1✔
748
      }
749
      case 'C': {
750
        const p = geo.vec(seg[3], seg[4])
3✔
751
        preC = p
3✔
752
        absP = geo.vec(seg[5], seg[6])
3✔
753
        preCType = 3
3✔
754
        break
3✔
755
      }
756
      case 'c': {
757
        const p = geo.vec(seg[3] + absP.x, seg[4] + absP.y)
1✔
758
        preC = p
1✔
759
        absP = geo.vec(seg[5] + absP.x, seg[6] + absP.y)
1✔
760
        preCType = 3
1✔
761
        break
1✔
762
      }
763
      case 'S': {
764
        const p = preCType === 3 ? geo.lerpPoint(preC, absP, 2) : absP
2✔
765
        preC = p
2✔
766
        absP = geo.vec(seg[3], seg[4])
2✔
767
        preCType = 3
2✔
768
        break
2✔
769
      }
770
      case 's': {
771
        const p = preCType === 3 ? geo.lerpPoint(preC, absP, 2) : absP
1!
772
        preC = p
1✔
773
        absP = geo.vec(seg[3] + absP.x, seg[4] + absP.y)
1✔
774
        preCType = 3
1✔
775
        break
1✔
776
      }
777
      case 'A': {
778
        const p = geo.vec(seg[6], seg[7])
1✔
779
        absP = preC = p
1✔
780
        preCType = 1
1✔
781
        break
1✔
782
      }
783
      case 'a': {
784
        const p = geo.vec(seg[6] + absP.x, seg[7] + absP.y)
1✔
785
        absP = preC = p
1✔
786
        preCType = 1
1✔
787
        break
1✔
788
      }
789
      case 'Z':
790
      case 'z': {
791
        absP = preC = startP
3✔
792
        preCType = 1
3✔
793
        break
3✔
794
      }
795
      default:
796
        throw getUnknownError()
×
797
    }
798

799
    controls.push(preC)
65✔
800
    points.push(absP)
65✔
801
  }
802

803
  return { points, controls }
24✔
804
}
805

806
function isCurveCommand(c: string) {
807
  return /Q|q|T|t|C|c|S|s|A|a/.test(c)
3✔
808
}
809

810
/**
811
 * The first segment has to be either "M", "m", "L" or "l".
812
 *
813
 * The last segment will be converted to normalized value.
814
 * e.g. [m, l, v, z] => [M, v, l, z]
815
 *
816
 * "T", "t", "S" or "s" will be converted to "Q", "q", "C" or "c"
817
 */
818
export function reversePath(segments: PathSegmentRaw[]): PathSegmentRaw[] {
1✔
819
  if (segments.length < 2) return segments
19!
820

821
  const ret: PathSegmentRaw[] = []
19✔
822

823
  const { points: absPoints, controls: absContolPoints } =
824
    getPathAbsPoints(segments)
19✔
825

826
  const length = segments.length
19✔
827
  let current: PathSegmentRaw
828
  let absP: IVec2
829
  let closeCount = false
19✔
830
  for (let i = length - 1; 0 <= i; i--) {
19✔
831
    current = segments[i]
59✔
832
    absP = absPoints[i === 0 ? length - 1 : i - 1]
59✔
833

834
    switch (current[0]) {
59✔
835
      case 'M':
59✔
836
        if (closeCount) {
17✔
837
          if (isCurveCommand(ret[ret.length - 1][0])) {
3✔
838
            ret.push(['Z'])
1✔
839
          } else {
840
            ret[ret.length - 1] = ['Z']
2✔
841
          }
842
          closeCount = false
3✔
843
        }
844
        ret.push([current[0], absP.x, absP.y])
17✔
845
        break
17✔
846
      case 'm':
847
        if (closeCount) {
1!
848
          if (isCurveCommand(ret[ret.length - 1][0])) {
×
849
            ret.push(['z'])
×
850
          } else {
851
            ret[ret.length - 1] = ['z']
×
852
          }
853
          closeCount = false
×
854
        }
855
        if (i === 0) {
1!
856
          ret.push(['M', absP.x, absP.y])
1✔
857
        } else {
858
          ret.push([current[0], -current[1], -current[2]])
×
859
        }
860
        break
1✔
861
      case 'L':
862
        if (closeCount && i === 0) {
12!
863
          if (isCurveCommand(ret[ret.length - 1][0])) {
×
864
            ret.push(['Z'])
×
865
          } else {
866
            ret[ret.length - 1] = ['Z']
×
867
          }
868
          closeCount = false
×
869
        }
870
        ret.push([current[0], absP.x, absP.y])
12✔
871
        break
12✔
872
      case 'l':
873
        if (closeCount && i === 0) {
5!
874
          if (isCurveCommand(ret[ret.length - 1][0])) {
×
875
            ret.push(['z'])
×
876
          } else {
877
            ret[ret.length - 1] = ['z']
×
878
          }
879
          closeCount = false
×
880
        }
881
        if (i === 0) {
5✔
882
          ret.push(['L', absP.x, absP.y])
1✔
883
        } else {
884
          ret.push([current[0], -current[1], -current[2]])
4✔
885
        }
886
        break
5✔
887
      case 'H':
888
        ret.push([current[0], absP.x])
1✔
889
        break
1✔
890
      case 'h':
891
        ret.push([current[0], -current[1]])
1✔
892
        break
1✔
893
      case 'V':
894
        ret.push([current[0], absP.y])
1✔
895
        break
1✔
896
      case 'v':
897
        ret.push([current[0], -current[1]])
1✔
898
        break
1✔
899
      case 'Q': {
900
        ret.push([current[0], current[1], current[2], absP.x, absP.y])
5✔
901
        break
5✔
902
      }
903
      case 'q': {
904
        ret.push([
1✔
905
          current[0],
906
          current[1] - current[3],
907
          current[2] - current[4],
908
          -current[3],
909
          -current[4],
910
        ])
911
        break
1✔
912
      }
913
      case 'T': {
914
        const c = absContolPoints[i]
1✔
915
        ret.push(['Q', c.x, c.y, absP.x, absP.y])
1✔
916
        break
1✔
917
      }
918
      case 't': {
919
        const b = absPoints[i]
1✔
920
        const c = absContolPoints[i]
1✔
921
        ret.push(['q', c.x - b.x, c.y - b.y, -current[1], -current[2]])
1✔
922
        break
1✔
923
      }
924
      case 'C': {
925
        ret.push([
3✔
926
          current[0],
927
          current[3],
928
          current[4],
929
          current[1],
930
          current[2],
931
          absP.x,
932
          absP.y,
933
        ])
934
        break
3✔
935
      }
936
      case 'c': {
937
        ret.push([
1✔
938
          current[0],
939
          current[3] - current[5],
940
          current[4] - current[6],
941
          current[1] - current[5],
942
          current[2] - current[6],
943
          -current[5],
944
          -current[6],
945
        ])
946
        break
1✔
947
      }
948
      case 'S': {
949
        const c = absContolPoints[i]
2✔
950
        ret.push(['C', current[1], current[2], c.x, c.y, absP.x, absP.y])
2✔
951
        break
2✔
952
      }
953
      case 's': {
954
        const b = absPoints[i]
1✔
955
        const c = absContolPoints[i]
1✔
956
        ret.push([
1✔
957
          'c',
958
          current[1] - current[3],
959
          current[2] - current[4],
960
          c.x - b.x,
961
          c.y - b.y,
962
          -current[3],
963
          -current[4],
964
        ])
965
        break
1✔
966
      }
967
      case 'A': {
968
        ret.push([
1✔
969
          current[0],
970
          current[1],
971
          current[2],
972
          current[3],
973
          current[4],
974
          !current[5],
975
          absP.x,
976
          absP.y,
977
        ])
978
        break
1✔
979
      }
980
      case 'a': {
981
        ret.push([
1✔
982
          current[0],
983
          current[1],
984
          current[2],
985
          current[3],
986
          current[4],
987
          !current[5],
988
          -current[6],
989
          -current[7],
990
        ])
991
        break
1✔
992
      }
993
      case 'Z':
994
        closeCount = true
2✔
995
        ret.push(['L', absP.x, absP.y])
2✔
996
        break
2✔
997
      case 'z': {
998
        closeCount = true
1✔
999
        const absPP = absPoints[i]
1✔
1000
        ret.push(['l', absP.x - absPP.x, absP.y - absPP.y])
1✔
1001
        break
1✔
1002
      }
1003
    }
1004
  }
1005

1006
  ret.unshift(ret.pop()!)
19✔
1007

1008
  return ret
19✔
1009
}
1010

1011
/**
1012
 * Slide segments.
1013
 * Relative segments will not be slided by this function.
1014
 */
1015
export function slidePath(
1✔
1016
  segments: PathSegmentRaw[],
1017
  diff: IVec2
1018
): PathSegmentRaw[] {
1019
  return segments.map((current) => {
2✔
1020
    const slided: PathSegmentRaw = [...current]
20✔
1021
    switch (slided[0]) {
20✔
1022
      case 'H':
20✔
1023
        slided[1] += diff.x
1✔
1024
        break
1✔
1025
      case 'V':
1026
        slided[1] += diff.y
1✔
1027
        break
1✔
1028
      case 'A':
1029
        slided[6] += diff.x
1✔
1030
        slided[7] += diff.y
1✔
1031
        break
1✔
1032
      default:
1033
        if (slided[0] === slided[0].toUpperCase()) {
17✔
1034
          for (let i = 1; i < slided.length - 1; i += 2) {
7✔
1035
            ;(slided[i] as number) += diff.x
10✔
1036
            ;(slided[i + 1] as number) += diff.y
10✔
1037
          }
1038
        }
1039
        break
17✔
1040
    }
1041
    return slided
20✔
1042
  })
1043
}
1044

1045
/**
1046
 * Scale segments.
1047
 * Both abstract and relative segments will be scaled by this function.
1048
 */
1049
export function scalePath(
1✔
1050
  segments: PathSegmentRaw[],
1051
  scale: IVec2
1052
): PathSegmentRaw[] {
1053
  return segments.map((current) => {
5✔
1054
    const slided: PathSegmentRaw = [...current]
26✔
1055
    switch (slided[0]) {
26✔
1056
      case 'H':
32✔
1057
      case 'h':
1058
        slided[1] *= scale.x
2✔
1059
        break
2✔
1060
      case 'V':
1061
      case 'v':
1062
        slided[1] *= scale.y
2✔
1063
        break
2✔
1064
      case 'A':
1065
      case 'a':
1066
        slided[1] *= Math.abs(scale.x)
5✔
1067
        slided[2] *= Math.abs(scale.y)
5✔
1068
        if (scale.x * scale.y < 0) {
5✔
1069
          slided[5] = !slided[5]
2✔
1070
        }
1071
        slided[6] *= scale.x
5✔
1072
        slided[7] *= scale.y
5✔
1073
        break
5✔
1074
      default:
1075
        for (let i = 1; i < slided.length - 1; i += 2) {
17✔
1076
          ;(slided[i] as number) *= scale.x
23✔
1077
          ;(slided[i + 1] as number) *= scale.y
23✔
1078
        }
1079
        break
17✔
1080
    }
1081
    return slided
26✔
1082
  })
1083
}
1084

1085
function convertHVToL(segments: PathSegmentRaw[]): PathSegmentRaw[] {
1086
  // If neither "H" nor "V" exists, abstract points doesn't have to be computed.
1087
  const absVHExisted = segments.some((s) => /H|V/.test(s[0]))
12✔
1088
  const { points } = getPathAbsPoints(absVHExisted ? segments : [])
5✔
1089

1090
  return segments.map((s, i) => {
5✔
1091
    switch (s[0]) {
14✔
1092
      case 'H':
14✔
1093
        return ['L', s[1], points[i].y]
1✔
1094
      case 'h':
1095
        return ['l', s[1], 0]
1✔
1096
      case 'V':
1097
        return ['L', points[i].x, s[1]]
1✔
1098
      case 'v':
1099
        return ['l', 0, s[1]]
1✔
1100
      default:
1101
        return s
10✔
1102
    }
1103
  })
1104
}
1105

1106
/**
1107
 * Rotate segments.
1108
 * Both abstract and relative segments will be rotated by this function.
1109
 * "H", "h", "V" and "v" will be converted to "L" or "l"
1110
 */
1111
export function rotatePath(
1✔
1112
  segments: PathSegmentRaw[],
1113
  radian: number
1114
): PathSegmentRaw[] {
1115
  const sin = Math.sin(radian)
5✔
1116
  const cos = Math.cos(radian)
5✔
1117
  return convertHVToL(segments).map((current) => {
5✔
1118
    const slided: PathSegmentRaw = [...current]
14✔
1119
    switch (slided[0]) {
14✔
1120
      case 'A':
15✔
1121
      case 'a': {
1122
        slided[3] += (radian * 180) / Math.PI
1✔
1123
        const x = slided[6]
1✔
1124
        const y = slided[7]
1✔
1125
        slided[6] = cos * x - sin * y
1✔
1126
        slided[7] = sin * x + cos * y
1✔
1127
        break
1✔
1128
      }
1129
      default:
1130
        for (let i = 1; i < slided.length - 1; i += 2) {
13✔
1131
          const x = slided[i] as number
13✔
1132
          const y = slided[i + 1] as number
13✔
1133
          ;(slided[i] as number) = cos * x - sin * y
13✔
1134
          ;(slided[i + 1] as number) = sin * x + cos * y
13✔
1135
        }
1136
        break
13✔
1137
    }
1138
    return slided
14✔
1139
  })
1140
}
1141

1142
/**
1143
 * Parse path d string and approximate it as a polyline
1144
 * Note:
1145
 * - Jump information by M/m commands doesn't remain in a polyline
1146
 * - Z/z commands are ignored => The tail point doesn't become the same as the head one by these commands
1147
 * @param dStr d string of path element
1148
 * @return approximated polyline
1149
 */
1150
export function parsePathD(
1✔
1151
  dStr: string,
1152
  split = configs.bezierSplitSize
25✔
1153
): IVec2[] {
1154
  const _split = Math.max(1, split)
27✔
1155
  let ret: IVec2[] = []
27✔
1156
  let step = 1 / _split
27✔
1157
  parsePathSegments(dStr).forEach((seg) => {
27✔
1158
    if (seg.command === 'Z' || seg.command === 'z') return
69✔
1159

1160
    if (seg.curve) {
66✔
1161
      for (let i = 1; i <= _split; i++) {
24✔
1162
        ret.push(seg.lerpFn(step * i))
223✔
1163
      }
1164
    } else {
1165
      ret.push(seg.segment[1])
42✔
1166
    }
1167
  })
1168
  return ret
27✔
1169
}
1170

1171
/**
1172
 * pathタグを解析する
1173
 * @param svgPath SVGのpathタグDOM
1174
 * @return 座標リスト
1175
 */
1176
export function parsePath(svgPath: SVGPathElement): IVec2[] {
1✔
1177
  const dStr = svgPath.getAttribute('d')
25✔
1178
  return dStr
25✔
1179
    ? adoptTransform(svgPath.getAttribute('transform'), parsePathD(dStr))
25✔
1180
    : []
1181
}
1182

1183
/**
1184
 * rectタグを解析する
1185
 * @param SVGのrectタグDOM
1186
 * @return 座標リスト
1187
 */
1188
export function parseRect(svgRect: SVGRectElement): IVec2[] {
1✔
1189
  const x = _parseFloat(svgRect.getAttribute('x') || '0')
4!
1190
  const y = _parseFloat(svgRect.getAttribute('y') || '0')
4!
1191
  const width = _parseFloat(svgRect.getAttribute('width') || '0')
4!
1192
  const height = _parseFloat(svgRect.getAttribute('height') || '0')
4!
1193

1194
  // トランスフォーム
1195
  return adoptTransform(svgRect.getAttribute('transform'), [
4✔
1196
    geo.vec(x, y),
1197
    geo.vec(x + width, y),
1198
    geo.vec(x + width, y + height),
1199
    geo.vec(x, y + height),
1200
  ])
1201
}
1202

1203
/**
1204
 * ellipseタグを解析する
1205
 * @param svgEllipse SVGのellipseタグDOM
1206
 * @return 座標リスト
1207
 */
1208
export function parseEllipse(svgEllipse: SVGEllipseElement): IVec2[] {
1✔
1209
  const cx = _parseFloat(svgEllipse.getAttribute('cx') || '0')
2✔
1210
  const cy = _parseFloat(svgEllipse.getAttribute('cy') || '0')
2✔
1211
  const rx = _parseFloat(svgEllipse.getAttribute('rx') || '1')
2!
1212
  const ry = _parseFloat(svgEllipse.getAttribute('ry') || '1')
2!
1213

1214
  // トランスフォーム
1215
  return adoptTransform(
2✔
1216
    svgEllipse.getAttribute('transform'),
1217
    geo.approximateArc(
1218
      rx,
1219
      ry,
1220
      0,
1221
      Math.PI * 2,
1222
      geo.vec(cx, cy),
1223
      0,
1224
      configs.ellipseSplitSize
1225
    )
1226
  )
1227
}
1228

1229
/**
1230
 * circleタグを解析する
1231
 * @param svgCircle  SVGのcircleタグDOM
1232
 * @return 座標リスト
1233
 */
1234
export function parseCircle(svgCircle: SVGCircleElement): IVec2[] {
1✔
1235
  const cx = _parseFloat(svgCircle.getAttribute('cx') || '0')
3✔
1236
  const cy = _parseFloat(svgCircle.getAttribute('cy') || '0')
3✔
1237
  const r = _parseFloat(svgCircle.getAttribute('r') || '1')
3!
1238

1239
  // トランスフォーム
1240
  return adoptTransform(
3✔
1241
    svgCircle.getAttribute('transform'),
1242
    geo.approximateArc(
1243
      r,
1244
      r,
1245
      0,
1246
      Math.PI * 2,
1247
      geo.vec(cx, cy),
1248
      0,
1249
      configs.ellipseSplitSize
1250
    )
1251
  )
1252
}
1253

1254
/**
1255
 * transformを行う
1256
 * @param commandStr コマンド文字列
1257
 * @param points 変換前座標リスト
1258
 * @return 変形後座標リスト
1259
 */
1260
export function adoptTransform(
1✔
1261
  commandStr: string | null,
1262
  points: IVec2[]
1263
): IVec2[] {
1264
  if (!commandStr) return points
43✔
1265

1266
  let ret: IVec2[] = geo.cloneVectors(points)
13✔
1267
  // 複数コマンドの場合もあるのでループ
1268
  const commandList = commandStr.split(/\)/)
13✔
1269
  commandList.forEach((current) => {
13✔
1270
    const tmp = current.split(/\(/)
27✔
1271
    if (tmp.length === 2) {
27✔
1272
      const command = tmp[0].trim().toLowerCase()
14✔
1273
      const params = parseNumbers(tmp[1])
14✔
1274

1275
      switch (command) {
14✔
1276
        case 'matrix': {
14✔
1277
          ret = geo.transform(ret, params)
1✔
1278
          break
1✔
1279
        }
1280
        case 'translate': {
1281
          ret = ret.map((p) => geo.vec(p.x + params[0], p.y + params[1]))
50✔
1282
          break
6✔
1283
        }
1284
        case 'scale': {
1285
          const scaleX = params[0]
2✔
1286
          // XY等倍の場合を考慮
1287
          let scaleY = params[0]
2✔
1288
          if (params.length > 1) {
2✔
1289
            scaleY = params[1]
1✔
1290
          }
1291
          ret = ret.map((p) => geo.vec(p.x * scaleX, p.y * scaleY))
2✔
1292
          break
2✔
1293
        }
1294
        case 'rotate': {
1295
          // 回転基準点
1296
          let base: IVec2 = geo.vec(0, 0)
2✔
1297
          if (params.length > 2) {
2✔
1298
            base = geo.vec(params[1], params[2])
1✔
1299
          }
1300
          ret = ret.map((p) => geo.rotate(p, (params[0] * Math.PI) / 180, base))
2✔
1301
          break
2✔
1302
        }
1303
        case 'skewx': {
1304
          ret = ret.map((p) =>
1✔
1305
            geo.vec(p.x + Math.tan((params[0] * Math.PI) / 180) * p.y, p.y)
1✔
1306
          )
1307
          break
1✔
1308
        }
1309
        case 'skewy': {
1310
          ret = ret.map((p) =>
2✔
1311
            geo.vec(p.x, p.y + Math.tan((params[0] * Math.PI) / 180) * p.x)
2✔
1312
          )
1313
          break
2✔
1314
        }
1315
      }
1316
    }
1317
  })
1318

1319
  return ret
13✔
1320
}
1321

1322
// All commands (BbRr isn't supported)
1323
const allCommand = /M|m|L|l|H|h|V|v|C|c|S|s|Q|q|T|t|A|a|Z|z/g
1✔
1324

1325
/**
1326
 * pathタグd属性文字列を分割する
1327
 * @param dString pathのd要素文字列
1328
 * @return コマンド単位の情報配列の配列
1329
 */
1330
export function splitD(dString: string): string[][] {
1✔
1331
  // 要素分割
1332
  const strList = dString
53✔
1333
    .replace(allCommand, ' $& ')
1334
    // Insert space before each signature, but don't destruct exponent exporession such as 2.2e-10.
1335
    .replace(/([^e])(-|\+)/g, '$1 $2')
1336
    .split(/,| /)
1337
    .filter((str) => str)
949✔
1338
  // 直前のコマンド
1339
  let pastCommand = 'M'
53✔
1340

1341
  const ret: string[][] = []
53✔
1342
  for (let i = 0; i < strList.length; ) {
53✔
1343
    const info: string[] = []
192✔
1344
    // Check if a command exists
1345
    if (strList[i].match(allCommand)) {
192✔
1346
      info.push(strList[i])
190✔
1347
      pastCommand = info[0]
190✔
1348
      i++
190✔
1349
    } else if (pastCommand.toUpperCase() !== 'Z') {
2✔
1350
      // Reuse previous command
1351
      // Avoid reusing 'Z' that can cause infinite loop
1352
      info.push(pastCommand)
2✔
1353
    }
1354

1355
    switch (info[0].toUpperCase()) {
192✔
1356
      case 'Z':
406!
1357
        break
14✔
1358
      case 'V':
1359
      case 'H':
1360
        info.push(strList[i])
8✔
1361
        i += 1
8✔
1362
        break
8✔
1363
      case 'M':
1364
      case 'L':
1365
      case 'T':
1366
        info.push(strList[i], strList[i + 1])
138✔
1367
        i += 2
138✔
1368
        break
138✔
1369
      case 'Q':
1370
      case 'S':
1371
        info.push(strList[i], strList[i + 1], strList[i + 2], strList[i + 3])
17✔
1372
        i += 4
17✔
1373
        break
17✔
1374
      case 'C':
1375
        info.push(
8✔
1376
          strList[i],
1377
          strList[i + 1],
1378
          strList[i + 2],
1379
          strList[i + 3],
1380
          strList[i + 4],
1381
          strList[i + 5]
1382
        )
1383
        i += 6
8✔
1384
        break
8✔
1385
      case 'A':
1386
        info.push(
7✔
1387
          strList[i],
1388
          strList[i + 1],
1389
          strList[i + 2],
1390
          strList[i + 3],
1391
          strList[i + 4],
1392
          strList[i + 5],
1393
          strList[i + 6]
1394
        )
1395
        i += 7
7✔
1396
        break
7✔
1397
      default:
1398
        throw getUnknownError()
×
1399
    }
1400

1401
    ret.push(info)
192✔
1402
  }
1403

1404
  return ret
53✔
1405
}
1406

1407
/**
1408
 * svg文字列を生成する
1409
 * @param pathList path情報リスト
1410
 * @return xml文字列
1411
 */
1412
export function serializeSvgString(pathList: ISvgPath[]): string {
1✔
1413
  const svg = serializeSvg(pathList)
×
1414
  const xmlSerializer = new XMLSerializer()
×
1415
  const textXml = xmlSerializer.serializeToString(svg)
×
1416
  return textXml
×
1417
}
1418

1419
/**
1420
 * svgタグを生成する
1421
 * @param pathList path情報リスト
1422
 * @return svgタグ
1423
 */
1424
export function serializeSvg(pathList: ISvgPath[]): SVGElement {
1✔
1425
  const dom = document.createElementNS(HTTP_SVG, 'svg')
1✔
1426

1427
  // キャンバスサイズ
1428
  let width = 1
1✔
1429
  let height = 1
1✔
1430

1431
  pathList.forEach((path) => {
1✔
1432
    dom.appendChild(serializePath(path.d, path.style))
1✔
1433
    path.d.forEach((p) => {
1✔
1434
      width = Math.max(width, p.x)
3✔
1435
      height = Math.max(height, p.y)
3✔
1436
    })
1437
  })
1438

1439
  width *= 1.1
1✔
1440
  height *= 1.1
1✔
1441

1442
  dom.setAttribute('width', `${width}`)
1✔
1443
  dom.setAttribute('height', `${height}`)
1✔
1444

1445
  return dom
1✔
1446
}
1447

1448
/**
1449
 * pathタグを生成する
1450
 * @param pointList 座標リスト
1451
 * @param style スタイル情報
1452
 * @return pathタグ
1453
 */
1454
export function serializePath(
1✔
1455
  pointList: IVec2[],
1456
  style: ISvgStyle
1457
): SVGPathElement {
1458
  const dom = document.createElementNS(HTTP_SVG, 'path')
2✔
1459
  dom.setAttribute('d', serializePointList(pointList))
2✔
1460
  dom.setAttribute('style', serializeStyle(style))
2✔
1461
  return dom
2✔
1462
}
1463

1464
/**
1465
 * 座標リストをd属性文字列に変換する
1466
 * @param pointList 座標リスト
1467
 * @param open 閉じないフラグ
1468
 * @return d属性文字列
1469
 */
1470
export function serializePointList(pointList: IVec2[], open?: boolean): string {
1✔
1471
  if (pointList.length === 0) return ''
6✔
1472
  const [head, ...body] = pointList
5✔
1473
  return (
5✔
1474
    `M ${head.x},${head.y}` +
1475
    body.map((p) => ` L ${p.x},${p.y}`).join('') +
8✔
1476
    (open ? '' : ' Z')
5✔
1477
  )
1478
}
1479

1480
/**
1481
 * デフォルトstyle作成
1482
 * @return スタイルオブジェクト
1483
 */
1484
export function createStyle() {
1✔
1485
  return {
55✔
1486
    fill: false,
1487
    fillGlobalAlpha: 1,
1488
    fillStyle: '',
1489
    lineCap: 'butt',
1490
    lineDash: [],
1491
    lineJoin: 'bevel',
1492
    lineWidth: 1,
1493
    stroke: false,
1494
    strokeGlobalAlpha: 1,
1495
    strokeStyle: '',
1496
  }
1497
}
1498

1499
/**
1500
 * pathタグのスタイルを取得する
1501
 * @param svgPath SVGのpathタグDOM
1502
 * @return スタイルオブジェクト
1503
 */
1504
export function parseTagStyle(svgPath: SVGElement): ISvgStyle {
1✔
1505
  // スタイル候補要素リスト
1506
  const styleObject: { [key: string]: string } = {}
51✔
1507

1508
  svgPath.getAttributeNames().forEach((name) => {
51✔
1509
    const attr = svgPath.getAttributeNode(name)
123✔
1510
    if (!attr) return
123!
1511
    styleObject[attr.name] = attr.value
123✔
1512
  })
1513

1514
  const styleAttr = svgPath.getAttributeNode('style')
51✔
1515
  if (styleAttr) {
51✔
1516
    // style要素から取得
1517
    const styleStr = styleAttr.value
14✔
1518
    styleStr.split(';').forEach((elem: string) => {
14✔
1519
      const splited = elem.split(':')
17✔
1520
      if (splited.length !== 2) return
17✔
1521
      styleObject[splited[0].trim()] = splited[1].trim()
15✔
1522
    })
1523
  }
1524

1525
  return Object.entries(styleObject).reduce<ISvgStyle>((ret, [key, val]) => {
51✔
1526
    switch (key.toLowerCase()) {
137✔
1527
      case 'fill':
137✔
1528
        if (val === 'none') {
19✔
1529
          ret.fillStyle = ''
3✔
1530
          ret.fill = false
3✔
1531
        } else {
1532
          ret.fillStyle = val
16✔
1533
          ret.fill = true
16✔
1534
        }
1535
        break
19✔
1536
      case 'stroke':
1537
        if (val === 'none') {
4✔
1538
          ret.strokeStyle = ''
2✔
1539
          ret.stroke = false
2✔
1540
        } else {
1541
          ret.strokeStyle = val
2✔
1542
          ret.stroke = true
2✔
1543
        }
1544
        break
4✔
1545
      case 'stroke-width':
1546
        ret.lineWidth = _parseFloat(val)
2✔
1547
        break
2✔
1548
      case 'stroke-opacity':
1549
        ret.strokeGlobalAlpha = _parseFloat(val)
2✔
1550
        break
2✔
1551
      case 'fill-opacity':
1552
        ret.fillGlobalAlpha = _parseFloat(val)
2✔
1553
        break
2✔
1554
      case 'stroke-linecap':
1555
        ret.lineCap = val
2✔
1556
        break
2✔
1557
      case 'stroke-linejoin':
1558
        ret.lineJoin = val
2✔
1559
        break
2✔
1560
      case 'stroke-dasharray':
1561
        if (val.toLowerCase() === 'none') {
3!
1562
          ret.lineDash = []
×
1563
        } else {
1564
          ret.lineDash = parseNumbers(val)
3✔
1565
        }
1566
        break
3✔
1567
      default:
1568
        // 無視
1569
        break
101✔
1570
    }
1571

1572
    return ret
137✔
1573
  }, createStyle())
1574
}
1575

1576
/**
1577
 * スタイル情報をstyle属性文字列に変換する
1578
 * @method serializeStyle
1579
 * @param style スタイル情報
1580
 * @return style属性文字列
1581
 */
1582
export function serializeStyle(style: ISvgStyle) {
1✔
1583
  let ret = ''
12✔
1584

1585
  // fill情報
1586
  if (!style.fill) {
12✔
1587
    ret += 'fill:none;'
9✔
1588
  } else {
1589
    ret += 'fill:' + style.fillStyle + ';'
3✔
1590
  }
1591
  if (style.fillGlobalAlpha) {
12✔
1592
    ret += 'fill-opacity:' + style.fillGlobalAlpha + ';'
12✔
1593
  }
1594

1595
  // stroke情報
1596
  if (!style.stroke) {
12✔
1597
    ret += 'stroke:none;'
11✔
1598
  } else {
1599
    ret += 'stroke:' + style.strokeStyle + ';'
1✔
1600
  }
1601
  if (style.lineWidth) {
12✔
1602
    ret += 'stroke-width:' + style.lineWidth + ';'
12✔
1603
  }
1604
  if (style.strokeGlobalAlpha) {
12✔
1605
    ret += 'stroke-opacity:' + style.strokeGlobalAlpha + ';'
12✔
1606
  }
1607
  if (style.lineCap) {
12✔
1608
    ret += 'stroke-linecap:' + style.lineCap + ';'
1✔
1609
  }
1610
  if (style.lineJoin) {
12✔
1611
    ret += 'stroke-linejoin:' + style.lineJoin + ';'
1✔
1612
  }
1613
  if (style.lineDash) {
12✔
1614
    if (style.lineDash.length > 0) {
12✔
1615
      ret += 'stroke-dasharray:' + style.lineDash.join(',') + ';'
1✔
1616
    } else {
1617
      ret += 'stroke-dasharray:none;'
11✔
1618
    }
1619
  }
1620

1621
  return ret
12✔
1622
}
1623

1624
/**
1625
 * パス分割
1626
 * @param path 対象パス
1627
 * @param line 分割線
1628
 * @return 分割後のパスリスト
1629
 */
1630
export function splitPath(path: ISvgPath, line: IVec2[]): ISvgPath[] {
1✔
1631
  let splited = geo.splitPolyByLine(path.d, line)
2✔
1632
  if (splited.length < 2) return [path]
2✔
1633

1634
  // 本体と回転方向が一致しているかで分類
1635
  const rootLoopwise = geo.getLoopwise(path.d)
1✔
1636
  const sameLoopwiseList: IVec2[][] = []
1✔
1637
  const oppositeLoopwiseList: IVec2[][] = []
1✔
1638
  if (path.included) {
1✔
1639
    path.included.forEach((s) => {
1✔
1640
      if (geo.getLoopwise(s) === rootLoopwise) {
4✔
1641
        sameLoopwiseList.push(s)
2✔
1642
      } else {
1643
        oppositeLoopwiseList.push(s)
2✔
1644
      }
1645
    })
1646
  }
1647

1648
  // 本体と同回転のものはそのまま分割
1649
  sameLoopwiseList.forEach((poly) => {
1✔
1650
    const sp = geo.splitPolyByLine(poly, line)
2✔
1651
    splited = [...splited, ...(sp.length > 0 ? sp : [poly])]
2✔
1652
  })
1653

1654
  // 本体と逆回転のものは特殊処理
1655
  const notPolyList: IVec2[][] = []
1✔
1656
  oppositeLoopwiseList.forEach((poly) => {
1✔
1657
    const sp = geo.splitPolyByLine(poly, line)
2✔
1658
    if (sp.length > 0) {
2!
1659
      // 分割されたらブーリアン差をとるために集める
1660
      notPolyList.push(poly)
2✔
1661
    } else {
1662
      // 分割なしならそのまま
1663
      splited.push(poly)
×
1664
    }
1665
  })
1666

1667
  // 切断されたくり抜き領域を差し引いたポリゴンを生成
1668
  const splitedAfterNot = splited.map((s) =>
1✔
1669
    notPolyList.reduce((p, c) => geo.getPolygonNotPolygon(p, c), s)
10✔
1670
  )
1671

1672
  return geo.getIncludedPolygonGroups(splitedAfterNot).map((group) => {
1✔
1673
    const [d, ...included] = group
4✔
1674
    return { d: d, included, style: path.style }
4✔
1675
  })
1676
}
1677

1678
/**
1679
 * ポリゴンリストをグルーピングしたパスリストに変換する
1680
 * @param polygons ポリゴンリスト
1681
 * @param style パススタイル
1682
 * @return パスリスト
1683
 */
1684
export function getGroupedPathList(
1✔
1685
  polygons: IVec2[][],
1686
  style: ISvgStyle = createStyle()
1✔
1687
): ISvgPath[] {
1688
  return geo.getIncludedPolygonGroups(polygons).map((group) => {
1✔
1689
    const [d, ...included] = group
2✔
1690
    return { d, included, style }
2✔
1691
  })
1692
}
1693

1694
/**
1695
 * convert affine matrix to transform attribute value
1696
 * @param matrix affine matrix
1697
 * @return transform attribute value
1698
 */
1699
export function affineToTransform(matrix: AffineMatrix): string {
1✔
1700
  return `matrix(${matrix.join(',')})`
1✔
1701
}
1702

1703
/**
1704
 * parse transform attribute value as affine matrix
1705
 * @param transform attribute value
1706
 * @return transform value
1707
 */
1708
export function parseTransform(transformStr: string): AffineMatrix {
1✔
1709
  const transformStrList = transformStr.split(')').map((s) => `${s})`)
24✔
1710
  const affines = transformStrList.map((str) => parseUnitTransform(str))
24✔
1711
  return geo.multiAffines(affines)
9✔
1712
}
1713

1714
function parseUnitTransform(str: string): AffineMatrix {
1715
  if (/translateX/.test(str)) return parseTranslateX(str)
24✔
1716
  if (/translateY/.test(str)) return parseTranslateY(str)
23✔
1717
  if (/translate/.test(str)) return parseTranslate(str)
22✔
1718
  if (/skewX/.test(str)) return parseSkewX(str)
16✔
1719
  if (/skewY/.test(str)) return parseSkewY(str)
15✔
1720
  if (/scaleX/.test(str)) return parseScaleX(str)
14✔
1721
  if (/scaleY/.test(str)) return parseScaleY(str)
13✔
1722
  if (/scale/.test(str)) return parseScale(str)
12✔
1723
  if (/rotate/.test(str)) return parseRotate(str)
10✔
1724
  if (/matrix/.test(str)) return parseMatrix(str)
9!
1725
  return [...geo.IDENTITY_AFFINE]
9✔
1726
}
1727

1728
function parseNumbers(str: string): number[] {
1729
  const list = str.trim().replace(/,/g, ' ').split(/ +/)
49✔
1730
  return list.map((s) => _parseFloat(s))
84✔
1731
}
1732

1733
/**
1734
 * parse transform attribute value of translate as affine matrix
1735
 * @param transform attribute value
1736
 * @return transform value
1737
 */
1738
export function parseTranslate(str: string): AffineMatrix {
1✔
1739
  const splited = str.match(/translate\((.+)\)/)
6✔
1740
  if (!splited || splited.length < 1) return [...geo.IDENTITY_AFFINE]
6!
1741

1742
  const numbers = parseNumbers(splited[1])
6✔
1743
  if (numbers.length < 1) {
6!
1744
    return [...geo.IDENTITY_AFFINE]
×
1745
  } else if (numbers.length === 1) {
6✔
1746
    return [1, 0, 0, 1, numbers[0], 0]
1✔
1747
  } else {
1748
    return [1, 0, 0, 1, numbers[0], numbers[1]]
5✔
1749
  }
1750
}
1751

1752
/**
1753
 * parse translateX attribute value of translate as affine matrix
1754
 * @param transform attribute value
1755
 * @return transform value
1756
 */
1757
export function parseTranslateX(str: string): AffineMatrix {
1✔
1758
  const splited = str.match(/translateX\((.+)\)/)
3✔
1759
  if (!splited || splited.length < 1) return [...geo.IDENTITY_AFFINE]
3!
1760

1761
  const numbers = parseNumbers(splited[1])
3✔
1762
  if (numbers.length < 1) {
3!
1763
    return [...geo.IDENTITY_AFFINE]
×
1764
  } else {
1765
    return [1, 0, 0, 1, numbers[0], 0]
3✔
1766
  }
1767
}
1768

1769
/**
1770
 * parse translateY attribute value of translate as affine matrix
1771
 * @param transform attribute value
1772
 * @return transform value
1773
 */
1774
export function parseTranslateY(str: string): AffineMatrix {
1✔
1775
  const splited = str.match(/translateY\((.+)\)/)
3✔
1776
  if (!splited || splited.length < 1) return [...geo.IDENTITY_AFFINE]
3!
1777

1778
  const numbers = parseNumbers(splited[1])
3✔
1779
  if (numbers.length < 1) {
3!
1780
    return [...geo.IDENTITY_AFFINE]
×
1781
  } else {
1782
    return [1, 0, 0, 1, 0, numbers[0]]
3✔
1783
  }
1784
}
1785

1786
/**
1787
 * parse skewX attribute value of translate as affine matrix
1788
 * @param transform attribute value
1789
 * @return transform value
1790
 */
1791
export function parseSkewX(str: string): AffineMatrix {
1✔
1792
  const splited = str.match(/skewX\((.+)\)/)
3✔
1793
  if (!splited || splited.length < 1) return [...geo.IDENTITY_AFFINE]
3!
1794

1795
  const numbers = parseNumbers(splited[1])
3✔
1796
  if (numbers.length < 1) {
3!
1797
    return [...geo.IDENTITY_AFFINE]
×
1798
  } else {
1799
    return [1, 0, Math.tan((numbers[0] * Math.PI) / 180), 1, 0, 0]
3✔
1800
  }
1801
}
1802

1803
/**
1804
 * parse skewY attribute value of translate as affine matrix
1805
 * @param transform attribute value
1806
 * @return transform value
1807
 */
1808
export function parseSkewY(str: string): AffineMatrix {
1✔
1809
  const splited = str.match(/skewY\((.+)\)/)
3✔
1810
  if (!splited || splited.length < 1) return [...geo.IDENTITY_AFFINE]
3!
1811

1812
  const numbers = parseNumbers(splited[1])
3✔
1813
  if (numbers.length < 1) {
3!
1814
    return [...geo.IDENTITY_AFFINE]
×
1815
  } else {
1816
    return [1, Math.tan((numbers[0] * Math.PI) / 180), 0, 1, 0, 0]
3✔
1817
  }
1818
}
1819

1820
/**
1821
 * parse transform attribute value of scale as affine matrix
1822
 * @param transform attribute value
1823
 * @return transform value
1824
 */
1825
export function parseScale(str: string): AffineMatrix {
1✔
1826
  const splited = str.match(/scale\((.+)\)/)
4✔
1827
  if (!splited || splited.length < 2) return [...geo.IDENTITY_AFFINE]
4!
1828

1829
  const numbers = parseNumbers(splited[1])
4✔
1830
  if (numbers.length < 1) {
4!
1831
    return [...geo.IDENTITY_AFFINE]
×
1832
  } else if (numbers.length === 1) {
4✔
1833
    return [numbers[0], 0, 0, numbers[0], 0, 0]
1✔
1834
  } else {
1835
    return [numbers[0], 0, 0, numbers[1], 0, 0]
3✔
1836
  }
1837
}
1838

1839
/**
1840
 * parse ScaleX attribute value of translate as affine matrix
1841
 * @param transform attribute value
1842
 * @return transform value
1843
 */
1844
export function parseScaleX(str: string): AffineMatrix {
1✔
1845
  const splited = str.match(/scaleX\((.+)\)/)
3✔
1846
  if (!splited || splited.length < 1) return [...geo.IDENTITY_AFFINE]
3!
1847

1848
  const numbers = parseNumbers(splited[1])
3✔
1849
  if (numbers.length < 1) {
3!
1850
    return [...geo.IDENTITY_AFFINE]
×
1851
  } else {
1852
    return [numbers[0], 0, 0, 1, 0, 0]
3✔
1853
  }
1854
}
1855

1856
/**
1857
 * parse ScaleY attribute value of translate as affine matrix
1858
 * @param transform attribute value
1859
 * @return transform value
1860
 */
1861
export function parseScaleY(str: string): AffineMatrix {
1✔
1862
  const splited = str.match(/scaleY\((.+)\)/)
3✔
1863
  if (!splited || splited.length < 1) return [...geo.IDENTITY_AFFINE]
3!
1864

1865
  const numbers = parseNumbers(splited[1])
3✔
1866
  if (numbers.length < 1) {
3!
1867
    return [...geo.IDENTITY_AFFINE]
×
1868
  } else {
1869
    return [1, 0, 0, numbers[0], 0, 0]
3✔
1870
  }
1871
}
1872

1873
/**
1874
 * parse transform attribute value of rotate as affine matrix
1875
 * @param transform attribute value
1876
 * @return transform value
1877
 */
1878
export function parseRotate(str: string): AffineMatrix {
1✔
1879
  const splited = str.match(/rotate\((.+)\)/)
3✔
1880
  if (!splited || splited.length < 2) return [...geo.IDENTITY_AFFINE]
3!
1881

1882
  const numbers = parseNumbers(splited[1])
3✔
1883
  if (parseNumbers.length < 1) return [...geo.IDENTITY_AFFINE]
3!
1884

1885
  const rad = (numbers[0] / 180) * Math.PI
3✔
1886
  const cos = Math.cos(rad)
3✔
1887
  const sin = Math.sin(rad)
3✔
1888
  const rot: AffineMatrix = [cos, sin, -sin, cos, 0, 0]
3✔
1889

1890
  if (numbers.length > 2) {
3✔
1891
    return geo.multiAffine(
1✔
1892
      geo.multiAffine([1, 0, 0, 1, numbers[1], numbers[2]], rot),
1893
      [1, 0, 0, 1, -numbers[1], -numbers[2]]
1894
    )
1895
  } else {
1896
    return rot
2✔
1897
  }
1898
}
1899

1900
/**
1901
 * parse transform attribute value of matrix as affine matrix
1902
 * @param transform attribute value
1903
 * @return transform value
1904
 */
1905
export function parseMatrix(str: string): AffineMatrix {
1✔
1906
  const splited = str.match(/matrix\((.+)\)/)
1✔
1907
  if (!splited || splited.length < 2) return [...geo.IDENTITY_AFFINE]
1!
1908

1909
  const numbers = parseNumbers(splited[1])
1✔
1910
  if (numbers.length < 5) return [...geo.IDENTITY_AFFINE]
1!
1911

1912
  return numbers.slice(0, 6) as AffineMatrix
1✔
1913
}
1914

1915
function getUnknownError(): Error {
1916
  return new Error(`Unexpected error`)
×
1917
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc