altium-toolkit 1.0.2 → 1.0.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.
@@ -132,7 +132,7 @@ export class PcbTextPrimitiveRenderer {
132
132
  metrics.lineHeight
133
133
  )
134
134
  const rectX = -layout.anchorX
135
- const rectY = layout.anchorY - layout.height
135
+ const rectY = -layout.anchorY
136
136
  const cornerRadius = Math.min(
137
137
  padding,
138
138
  layout.width / 2,
@@ -360,7 +360,21 @@ export class PcbTextPrimitiveRenderer {
360
360
  return null
361
361
  }
362
362
 
363
- context.font = PcbTextPrimitiveRenderer.#buildCanvasFont(text, fontSize)
363
+ const canvasFont = PcbTextPrimitiveRenderer.#buildCanvasFont(
364
+ text,
365
+ fontSize
366
+ )
367
+ if (
368
+ !PcbTextPrimitiveRenderer.#canMeasureCanvasFont(
369
+ text,
370
+ canvasFont,
371
+ fontSize
372
+ )
373
+ ) {
374
+ return null
375
+ }
376
+
377
+ context.font = canvasFont
364
378
 
365
379
  const measured = lines.map((line) => context.measureText(line || ' '))
366
380
  const ascent = PcbTextPrimitiveRenderer.#resolveMeasuredExtent(
@@ -388,6 +402,29 @@ export class PcbTextPrimitiveRenderer {
388
402
  }
389
403
  }
390
404
 
405
+ /**
406
+ * Checks whether canvas text metrics can be trusted for the requested
407
+ * imported font.
408
+ * @param {{ fontMetrics?: { averageAdvanceWidth?: number, unitsPerEm?: number } }} text Text record.
409
+ * @param {string} canvasFont Full canvas font shorthand.
410
+ * @param {number} fontSize Text font size.
411
+ * @returns {boolean}
412
+ */
413
+ static #canMeasureCanvasFont(text, canvasFont, fontSize) {
414
+ const fonts = globalThis.document?.fonts
415
+ if (typeof fonts?.check === 'function') {
416
+ return fonts.check(
417
+ PcbTextPrimitiveRenderer.#buildPrimaryCanvasFont(text, fontSize)
418
+ )
419
+ }
420
+
421
+ if (PcbTextPrimitiveRenderer.#fontMetricsAverageWidthRatio(text)) {
422
+ return false
423
+ }
424
+
425
+ return Boolean(canvasFont)
426
+ }
427
+
391
428
  /**
392
429
  * Resolves a measured text extent with a stable fallback.
393
430
  * @param {TextMetrics[]} measured Browser text metrics.
@@ -420,6 +457,25 @@ export class PcbTextPrimitiveRenderer {
420
457
  return `${style} ${weight} ${fontSize}px ${family}`
421
458
  }
422
459
 
460
+ /**
461
+ * Builds a single-family font shorthand for readiness checks.
462
+ * @param {{ fontFamily?: string, fontName?: string, isBold?: boolean, fontWeight?: number, isItalic?: boolean }} text Text record.
463
+ * @param {number} fontSize Text font size.
464
+ * @returns {string}
465
+ */
466
+ static #buildPrimaryCanvasFont(text, fontSize) {
467
+ const weight =
468
+ text?.isBold || Number(text?.fontWeight) >= 600 ? '700' : '400'
469
+ const style = text?.isItalic ? 'italic' : 'normal'
470
+ const family = PcbTextPrimitiveRenderer.#quoteFontFamily(
471
+ PcbTextPrimitiveRenderer.#cleanFontFamily(
472
+ text?.fontFamily || text?.fontName
473
+ )
474
+ )
475
+
476
+ return `${style} ${weight} ${fontSize}px ${family}`
477
+ }
478
+
423
479
  /**
424
480
  * Builds a browser font-family stack matching the 3D TrueType path.
425
481
  * @param {unknown} family Font family value.
@@ -195,7 +195,7 @@ export class SchematicContentLayout {
195
195
  ' ' +
196
196
  formatNumber(targetMinY) +
197
197
  ') scale(' +
198
- formatNumber(scale) +
198
+ SchematicContentLayout.#formatScale(scale) +
199
199
  ') translate(' +
200
200
  formatNumber(-bounds.minX) +
201
201
  ' ' +
@@ -252,20 +252,28 @@ export class SchematicContentLayout {
252
252
  return ''
253
253
  }
254
254
 
255
- const pivotX = margin
256
- const pivotY = height - margin
257
255
  const topLimit = margin + contentPadding * 0.2
258
256
  const bottomLimit = height - margin - footerReserve
259
257
  const rightLimit = width - margin
258
+ const usedWidth = bounds.maxX - bounds.minX
259
+ const usedHeight = bounds.maxY - bounds.minY
260
+
261
+ if (usedWidth <= 0 || usedHeight <= 0) {
262
+ return ''
263
+ }
264
+
260
265
  const scale = Math.min(
261
266
  targetScale,
262
- ...SchematicContentLayout.#resolvePivotScaleLimits(
263
- bounds,
264
- pivotX,
265
- pivotY,
267
+ SchematicContentLayout.#resolveContentScaleLimit(
268
+ bounds.minX,
269
+ bounds.maxX,
266
270
  margin,
271
+ rightLimit
272
+ ),
273
+ SchematicContentLayout.#resolveContentScaleLimit(
274
+ bounds.minY,
275
+ bounds.maxY,
267
276
  topLimit,
268
- rightLimit,
269
277
  bottomLimit
270
278
  )
271
279
  )
@@ -274,10 +282,18 @@ export class SchematicContentLayout {
274
282
  return ''
275
283
  }
276
284
 
277
- const projectedMinX = pivotX + (bounds.minX - pivotX) * scale
278
- const projectedMaxX = pivotX + (bounds.maxX - pivotX) * scale
279
- const projectedMinY = pivotY + (bounds.minY - pivotY) * scale
280
- const projectedMaxY = pivotY + (bounds.maxY - pivotY) * scale
285
+ const targetMinX =
286
+ SchematicContentLayout.#resolveCenteredContentTargetMinX(
287
+ bounds,
288
+ scale,
289
+ margin,
290
+ rightLimit
291
+ )
292
+ const projectedMinX = targetMinX
293
+ const projectedMaxX = targetMinX + usedWidth * scale
294
+ const targetMinY = bottomLimit - usedHeight * scale
295
+ const projectedMinY = targetMinY
296
+ const projectedMaxY = bottomLimit
281
297
 
282
298
  if (
283
299
  projectedMinX < margin - 0.01 ||
@@ -290,19 +306,73 @@ export class SchematicContentLayout {
290
306
 
291
307
  return (
292
308
  ' transform="translate(' +
293
- formatNumber(pivotX) +
309
+ formatNumber(targetMinX) +
294
310
  ' ' +
295
- formatNumber(pivotY) +
311
+ formatNumber(targetMinY) +
296
312
  ') scale(' +
297
- formatNumber(scale) +
313
+ SchematicContentLayout.#formatScale(scale) +
298
314
  ') translate(' +
299
- formatNumber(-pivotX) +
315
+ formatNumber(-bounds.minX) +
300
316
  ' ' +
301
- formatNumber(-pivotY) +
317
+ formatNumber(-bounds.minY) +
302
318
  ')"'
303
319
  )
304
320
  }
305
321
 
322
+ /**
323
+ * Resolves one scale cap that keeps a horizontally centered content
324
+ * envelope inside the sheet frame.
325
+ * @param {number} minCoordinate
326
+ * @param {number} maxCoordinate
327
+ * @param {number} minLimit
328
+ * @param {number} maxLimit
329
+ * @returns {number}
330
+ */
331
+ static #resolveContentScaleLimit(
332
+ minCoordinate,
333
+ maxCoordinate,
334
+ minLimit,
335
+ maxLimit
336
+ ) {
337
+ const usedWidth = maxCoordinate - minCoordinate
338
+ const availableWidth = maxLimit - minLimit
339
+
340
+ return usedWidth > 0 && availableWidth > 0
341
+ ? availableWidth / usedWidth
342
+ : Infinity
343
+ }
344
+
345
+ /**
346
+ * Formats SVG transform scales with enough precision to avoid visible
347
+ * placement drift on large schematic sheets.
348
+ * @param {number} value
349
+ * @returns {string}
350
+ */
351
+ static #formatScale(value) {
352
+ return Number(value).toFixed(4).replace(/0+$/, '').replace(/\.$/, '')
353
+ }
354
+
355
+ /**
356
+ * Resolves the target SVG x-coordinate for a scaled content envelope.
357
+ * @param {{ minX: number, maxX: number }} bounds
358
+ * @param {number} scale
359
+ * @param {number} leftLimit
360
+ * @param {number} rightLimit
361
+ * @returns {number}
362
+ */
363
+ static #resolveCenteredContentTargetMinX(
364
+ bounds,
365
+ scale,
366
+ leftLimit,
367
+ rightLimit
368
+ ) {
369
+ const scaledWidth = (bounds.maxX - bounds.minX) * scale
370
+ const availableWidth = rightLimit - leftLimit
371
+ const remainingWidth = Math.max(availableWidth - scaledWidth, 0)
372
+
373
+ return leftLimit + remainingWidth / 2
374
+ }
375
+
306
376
  /**
307
377
  * Resolves per-edge maximum scale factors for a bottom-left pivot.
308
378
  * @param {{ minX: number, minY: number, maxX: number, maxY: number }} bounds
@@ -436,11 +506,43 @@ export class SchematicContentLayout {
436
506
  const heightSlackRatio =
437
507
  (matchingSheet.height - requiredHeight) / requiredHeight
438
508
 
439
- return widthSlackRatio <= RELAXED_STANDARD_PAGE_MAX_SLACK_RATIO &&
440
- heightSlackRatio <= RELAXED_STANDARD_PAGE_MAX_SLACK_RATIO &&
441
- matchingSheet.width < width
442
- ? matchingSheet
443
- : null
509
+ if (
510
+ widthSlackRatio > RELAXED_STANDARD_PAGE_MAX_SLACK_RATIO ||
511
+ heightSlackRatio > RELAXED_STANDARD_PAGE_MAX_SLACK_RATIO ||
512
+ matchingSheet.width >= width
513
+ ) {
514
+ return null
515
+ }
516
+
517
+ return {
518
+ ...matchingSheet,
519
+ width: SchematicContentLayout.#resolveTightVirtualDimension(
520
+ requiredWidth,
521
+ matchingSheet.width,
522
+ margin
523
+ )
524
+ }
525
+ }
526
+
527
+ /**
528
+ * Drops near-zero standard-page rounding slack from the scale axis so
529
+ * custom sheets that almost fill a standard source size do not render with
530
+ * a visible artificial gutter.
531
+ * @param {number} requiredDimension
532
+ * @param {number} candidateDimension
533
+ * @param {number} margin
534
+ * @returns {number}
535
+ */
536
+ static #resolveTightVirtualDimension(
537
+ requiredDimension,
538
+ candidateDimension,
539
+ margin
540
+ ) {
541
+ const slack = candidateDimension - requiredDimension
542
+
543
+ return slack > 0 && slack <= margin
544
+ ? requiredDimension
545
+ : candidateDimension
444
546
  }
445
547
 
446
548
  /**
@@ -18,6 +18,7 @@ export class SchematicJunctionRenderer {
18
18
  * @param {{ x: number, y: number, width: number, direction?: 'left' | 'right' | 'up' | 'down' }[]} [ports]
19
19
  * @param {{ x: number, y: number, style?: number, powerPortDirection?: 'up' | 'down' | 'left' | 'right' }[]} [powerPorts]
20
20
  * @param {number} sheetHeight
21
+ * @param {{ x: number, y: number }[]} [authoredJunctions]
21
22
  * @returns {string}
22
23
  */
23
24
  static buildMarkup(
@@ -25,13 +26,15 @@ export class SchematicJunctionRenderer {
25
26
  crosses,
26
27
  ports = [],
27
28
  powerPorts = [],
28
- sheetHeight
29
+ sheetHeight,
30
+ authoredJunctions = []
29
31
  ) {
30
32
  return SchematicJunctionRenderer.#resolveJunctions(
31
33
  lines,
32
34
  crosses,
33
35
  ports,
34
- powerPorts
36
+ powerPorts,
37
+ authoredJunctions
35
38
  )
36
39
  .map(
37
40
  (junction) =>
@@ -57,9 +60,16 @@ export class SchematicJunctionRenderer {
57
60
  * @param {{ x: number, y: number }[]} crosses
58
61
  * @param {{ x: number, y: number, width: number, direction?: 'left' | 'right' | 'up' | 'down' }[]} ports
59
62
  * @param {{ x: number, y: number, style?: number, powerPortDirection?: 'up' | 'down' | 'left' | 'right' }[]} powerPorts
63
+ * @param {{ x: number, y: number }[]} authoredJunctions
60
64
  * @returns {{ x: number, y: number, color: string }[]}
61
65
  */
62
- static #resolveJunctions(lines, crosses, ports, powerPorts) {
66
+ static #resolveJunctions(
67
+ lines,
68
+ crosses,
69
+ ports,
70
+ powerPorts,
71
+ authoredJunctions
72
+ ) {
63
73
  const wireLines = lines.filter((line) =>
64
74
  SchematicJunctionRenderer.#isElectricalWireLine(line)
65
75
  )
@@ -69,6 +79,11 @@ export class SchematicJunctionRenderer {
69
79
  const visiblePowerPorts = powerPorts.filter((powerPort) =>
70
80
  SchematicJunctionRenderer.#isDrawablePowerPort(powerPort)
71
81
  )
82
+ const authoredJunctionKeys = new Set(
83
+ authoredJunctions.map((junction) =>
84
+ SchematicJunctionRenderer.#pointKey(junction)
85
+ )
86
+ )
72
87
 
73
88
  return SchematicJunctionRenderer.#collectCandidatePoints(
74
89
  wireLines,
@@ -77,6 +92,9 @@ export class SchematicJunctionRenderer {
77
92
  )
78
93
  .filter(
79
94
  (point) =>
95
+ !authoredJunctionKeys.has(
96
+ SchematicJunctionRenderer.#pointKey(point)
97
+ ) &&
80
98
  !SchematicJunctionRenderer.#hasNearbyCross(point, crosses)
81
99
  )
82
100
  .flatMap((point) => {
@@ -15,7 +15,7 @@ const MINIMUM_NOTE_TEXT_SIZE = 4
15
15
  export class SchematicNoteRenderer {
16
16
  /**
17
17
  * Builds one boxed schematic note/callout with wrapped text rows.
18
- * @param {{ x: number, y: number, color: string, fontSize?: number, fontFamily?: string, fontWeight?: number, cornerX?: number, cornerY?: number, fill?: string, borderColor?: string, isSolid?: boolean, showBorder?: boolean, textMargin?: number, noteLines?: string[] }} text
18
+ * @param {{ x: number, y: number, color: string, fontSize?: number, fontFamily?: string, fontWeight?: number, fontStyle?: string, cornerX?: number, cornerY?: number, fill?: string, borderColor?: string, isSolid?: boolean, showBorder?: boolean, textMargin?: number, noteLines?: string[] }} text
19
19
  * @param {number} sheetHeight
20
20
  * @returns {string}
21
21
  */
@@ -52,6 +52,18 @@ export class SchematicNoteRenderer {
52
52
  )
53
53
  const noteStroke = text.showBorder ? borderColor : 'none'
54
54
  const noteSourceLines = text.noteLines || []
55
+ const compactSingleLineNote =
56
+ SchematicNoteRenderer.#isCompactSingleLineNote(
57
+ noteSourceLines,
58
+ height,
59
+ requestedTextSize
60
+ )
61
+ const horizontalTextMargin =
62
+ SchematicNoteRenderer.#resolveHorizontalTextMargin(
63
+ textMargin,
64
+ compactSingleLineNote,
65
+ text
66
+ )
55
67
  const verticalTextMargin =
56
68
  SchematicNoteRenderer.#resolveVerticalTextMargin(
57
69
  textMargin,
@@ -60,14 +72,10 @@ export class SchematicNoteRenderer {
60
72
  )
61
73
  const layout = SchematicNoteRenderer.#resolveTextLayout(
62
74
  noteSourceLines,
63
- Math.max(width - textMargin * 2, requestedTextSize),
75
+ Math.max(width - horizontalTextMargin * 2, requestedTextSize),
64
76
  Math.max(height - verticalTextMargin * 2, requestedTextSize),
65
77
  requestedTextSize,
66
- SchematicNoteRenderer.#isCompactSingleLineNote(
67
- noteSourceLines,
68
- height,
69
- requestedTextSize
70
- )
78
+ compactSingleLineNote
71
79
  )
72
80
  const noteLines = layout.noteLines
73
81
  const textSize = layout.textSize
@@ -80,10 +88,11 @@ export class SchematicNoteRenderer {
80
88
  left,
81
89
  right,
82
90
  top,
83
- textMargin,
91
+ horizontalTextMargin,
84
92
  verticalTextMargin,
85
93
  lineHeight,
86
94
  textSize,
95
+ compactSingleLineNote,
87
96
  text
88
97
  )
89
98
  )
@@ -310,6 +319,25 @@ export class SchematicNoteRenderer {
310
319
  return visibleLineCount === 1 && height <= requestedTextSize * 1.5
311
320
  }
312
321
 
322
+ /**
323
+ * Resolves horizontal text padding for one note box.
324
+ * @param {number} textMargin
325
+ * @param {boolean} compactSingleLineNote
326
+ * @param {{ showBorder?: boolean }} text
327
+ * @returns {number}
328
+ */
329
+ static #resolveHorizontalTextMargin(
330
+ textMargin,
331
+ compactSingleLineNote,
332
+ text
333
+ ) {
334
+ if (compactSingleLineNote && text.showBorder === false) {
335
+ return 0
336
+ }
337
+
338
+ return textMargin
339
+ }
340
+
313
341
  /**
314
342
  * Reduces vertical padding for short note rectangles so readable text is
315
343
  * centered instead of scaled down to satisfy the default margin.
@@ -426,7 +454,8 @@ export class SchematicNoteRenderer {
426
454
  * @param {number} verticalTextMargin
427
455
  * @param {number} lineHeight
428
456
  * @param {number} textSize
429
- * @param {{ color: string, fontFamily?: string, fontWeight?: number }} text
457
+ * @param {boolean} compactSingleLineNote
458
+ * @param {{ color: string, fontFamily?: string, fontWeight?: number, fontStyle?: string }} text
430
459
  * @returns {string}
431
460
  */
432
461
  static #buildNoteLineMarkup(
@@ -439,10 +468,18 @@ export class SchematicNoteRenderer {
439
468
  verticalTextMargin,
440
469
  lineHeight,
441
470
  textSize,
471
+ compactSingleLineNote,
442
472
  text
443
473
  ) {
444
474
  const x = left + horizontalTextMargin
445
- const y = top + verticalTextMargin + textSize + index * lineHeight
475
+ const y =
476
+ top +
477
+ verticalTextMargin +
478
+ SchematicNoteRenderer.#resolveBaselineOffset(
479
+ textSize,
480
+ compactSingleLineNote
481
+ ) +
482
+ index * lineHeight
446
483
 
447
484
  if (/^_+$/.test(String(line || '').trim())) {
448
485
  return (
@@ -483,9 +520,37 @@ export class SchematicNoteRenderer {
483
520
  escapeHtml(text.fontFamily || 'Times New Roman') +
484
521
  '" font-weight="' +
485
522
  formatNumber(text.fontWeight || 400) +
523
+ SchematicNoteRenderer.#buildFontStyleAttribute(text.fontStyle) +
486
524
  '" xml:space="preserve">' +
487
525
  escapeHtml(line) +
488
526
  '</text>'
489
527
  )
490
528
  }
529
+
530
+ /**
531
+ * Builds an optional note font-style attribute.
532
+ * @param {string | undefined} fontStyle
533
+ * @returns {string}
534
+ */
535
+ static #buildFontStyleAttribute(fontStyle) {
536
+ if (!fontStyle || fontStyle === 'normal') {
537
+ return ''
538
+ }
539
+
540
+ return '" font-style="' + escapeHtml(fontStyle)
541
+ }
542
+
543
+ /**
544
+ * Resolves the text baseline offset for a rendered note line.
545
+ * @param {number} textSize
546
+ * @param {boolean} compactSingleLineNote
547
+ * @returns {number}
548
+ */
549
+ static #resolveBaselineOffset(textSize, compactSingleLineNote) {
550
+ if (compactSingleLineNote) {
551
+ return textSize * 0.85
552
+ }
553
+
554
+ return textSize
555
+ }
491
556
  }
@@ -66,7 +66,8 @@ export class SchematicPinSvgRenderer {
66
66
  const defaultNumberX =
67
67
  geometry.bodyX -
68
68
  SchematicPinSvgRenderer.#resolveHorizontalPinNumberClearance(
69
- outerMarkerStyle
69
+ outerMarkerStyle,
70
+ pin
70
71
  )
71
72
  const numberX = hasExplicitOwnerPinName
72
73
  ? SchematicOwnerPinLabelLayout.resolveExplicitOwnerPinNumberX(
@@ -98,7 +99,11 @@ export class SchematicPinSvgRenderer {
98
99
  texts.push(
99
100
  SchematicPinSvgRenderer.#buildPinNameTextMarkup(
100
101
  'schematic-pin-name',
101
- geometry.bodyX + (labelMode === 'name-only' ? 10 : 4),
102
+ geometry.bodyX +
103
+ SchematicPinSvgRenderer.#resolveHorizontalPinNameInset(
104
+ pin,
105
+ labelMode
106
+ ),
102
107
  projectedY + 3,
103
108
  pin,
104
109
  labelColor,
@@ -114,7 +119,8 @@ export class SchematicPinSvgRenderer {
114
119
  const defaultNumberX =
115
120
  geometry.bodyX +
116
121
  SchematicPinSvgRenderer.#resolveHorizontalPinNumberClearance(
117
- outerMarkerStyle
122
+ outerMarkerStyle,
123
+ pin
118
124
  )
119
125
  const numberX = hasExplicitOwnerPinName
120
126
  ? SchematicOwnerPinLabelLayout.resolveExplicitOwnerPinNumberX(
@@ -146,7 +152,11 @@ export class SchematicPinSvgRenderer {
146
152
  texts.push(
147
153
  SchematicPinSvgRenderer.#buildPinNameTextMarkup(
148
154
  'schematic-pin-name',
149
- geometry.bodyX - (labelMode === 'name-only' ? 10 : 4),
155
+ geometry.bodyX -
156
+ SchematicPinSvgRenderer.#resolveHorizontalPinNameInset(
157
+ pin,
158
+ labelMode
159
+ ),
150
160
  projectedY + 3,
151
161
  pin,
152
162
  labelColor,
@@ -363,11 +373,12 @@ export class SchematicPinSvgRenderer {
363
373
  }
364
374
 
365
375
  /**
366
- * Returns the horizontal pin-number clearance needed by an authored marker.
376
+ * Returns the horizontal pin-number clearance needed by the pin geometry.
367
377
  * @param {'single-in' | 'single-out' | 'double' | null} markerStyle
378
+ * @param {{ length?: number }} pin
368
379
  * @returns {number}
369
380
  */
370
- static #resolveHorizontalPinNumberClearance(markerStyle) {
381
+ static #resolveHorizontalPinNumberClearance(markerStyle, pin) {
371
382
  switch (markerStyle) {
372
383
  case 'double':
373
384
  return 17
@@ -375,10 +386,40 @@ export class SchematicPinSvgRenderer {
375
386
  case 'single-out':
376
387
  return 8
377
388
  default:
378
- return 2
389
+ return SchematicPinSvgRenderer.#resolveLongPinInset(pin, 2)
379
390
  }
380
391
  }
381
392
 
393
+ /**
394
+ * Returns the horizontal pin-name inset used inside the symbol body.
395
+ * @param {{ length?: number }} pin
396
+ * @param {'hidden' | 'number-only' | 'name-only' | 'name-and-number'} labelMode
397
+ * @returns {number}
398
+ */
399
+ static #resolveHorizontalPinNameInset(pin, labelMode) {
400
+ if (labelMode === 'name-only') {
401
+ return 10
402
+ }
403
+
404
+ return SchematicPinSvgRenderer.#resolveLongPinInset(pin, 4)
405
+ }
406
+
407
+ /**
408
+ * Adds extra text clearance for long connector-style pin stubs.
409
+ * @param {{ length?: number }} pin
410
+ * @param {number} fallback
411
+ * @returns {number}
412
+ */
413
+ static #resolveLongPinInset(pin, fallback) {
414
+ const length = Math.abs(Number(pin?.length || 0))
415
+
416
+ if (length < 30) {
417
+ return fallback
418
+ }
419
+
420
+ return fallback === 2 ? 10 : 8
421
+ }
422
+
382
423
  /**
383
424
  * Builds one or two authored outer-marker polygons for one horizontal pin.
384
425
  * @param {number} bodyX