altium-toolkit 0.1.0

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 (82) hide show
  1. package/AGENTS.md +67 -0
  2. package/COMMERCIAL-LICENSE.md +20 -0
  3. package/CONTRIBUTING.md +19 -0
  4. package/LICENSE +22 -0
  5. package/LICENSES/CC-BY-SA-4.0.txt +170 -0
  6. package/LICENSES/GPL-3.0-or-later.txt +232 -0
  7. package/NOTICE.md +32 -0
  8. package/README.md +116 -0
  9. package/docs/api.md +73 -0
  10. package/docs/model-format.md +36 -0
  11. package/docs/testing.md +25 -0
  12. package/examples/README.md +47 -0
  13. package/examples/arduino-uno/PcbThreeSceneRenderer.mjs +635 -0
  14. package/examples/arduino-uno/SvgViewportController.mjs +306 -0
  15. package/examples/arduino-uno/example.mjs +480 -0
  16. package/examples/arduino-uno/index.html +163 -0
  17. package/examples/arduino-uno/styles.css +552 -0
  18. package/examples/server.mjs +212 -0
  19. package/package.json +53 -0
  20. package/spec/library-scope.md +32 -0
  21. package/src/core/BinaryReader.mjs +127 -0
  22. package/src/core/altium/AltiumLayoutParser.mjs +485 -0
  23. package/src/core/altium/AltiumParser.mjs +1007 -0
  24. package/src/core/altium/AsciiRecordParser.mjs +151 -0
  25. package/src/core/altium/ParserUtils.mjs +173 -0
  26. package/src/core/altium/PcbBinaryPrimitiveParser.mjs +424 -0
  27. package/src/core/altium/PcbEmbeddedModelExtractor.mjs +505 -0
  28. package/src/core/altium/PcbModelParser.mjs +336 -0
  29. package/src/core/altium/PcbOutlineRasterizer.mjs +852 -0
  30. package/src/core/altium/PcbOutlineRecovery.mjs +957 -0
  31. package/src/core/altium/PcbStreamExtractor.mjs +210 -0
  32. package/src/core/altium/PrintableTextDecoder.mjs +156 -0
  33. package/src/core/altium/SchematicAnnotationParser.mjs +220 -0
  34. package/src/core/altium/SchematicBusEntryParser.mjs +48 -0
  35. package/src/core/altium/SchematicDirectiveParser.mjs +47 -0
  36. package/src/core/altium/SchematicImageParser.mjs +173 -0
  37. package/src/core/altium/SchematicJunctionParser.mjs +43 -0
  38. package/src/core/altium/SchematicMultipartOwnerMatcher.mjs +564 -0
  39. package/src/core/altium/SchematicNetlistBuilder.mjs +351 -0
  40. package/src/core/altium/SchematicPinParser.mjs +767 -0
  41. package/src/core/altium/SchematicPrimitiveParser.mjs +716 -0
  42. package/src/core/altium/SchematicSheetParser.mjs +241 -0
  43. package/src/core/altium/SchematicSheetStyleResolver.mjs +46 -0
  44. package/src/core/altium/SchematicStandaloneCalloutNormalizer.mjs +592 -0
  45. package/src/core/altium/SchematicTextParser.mjs +708 -0
  46. package/src/core/altium/SchematicTextPostProcessor.mjs +801 -0
  47. package/src/core/ole/OleCompoundDocument.mjs +439 -0
  48. package/src/core/ole/OleConstants.mjs +64 -0
  49. package/src/core/ole/OleDirectoryEntry.mjs +95 -0
  50. package/src/index.mjs +7 -0
  51. package/src/parser.mjs +21 -0
  52. package/src/renderers.mjs +15 -0
  53. package/src/scene3d.mjs +9 -0
  54. package/src/styles/altium-renderers.css +358 -0
  55. package/src/ui/BomTableRenderer.mjs +46 -0
  56. package/src/ui/PcbArcUtils.mjs +189 -0
  57. package/src/ui/PcbEdgeFacingGlyphNormalizer.mjs +808 -0
  58. package/src/ui/PcbFootprintPrimitiveSelector.mjs +128 -0
  59. package/src/ui/PcbScene3dBuilder.mjs +742 -0
  60. package/src/ui/PcbScene3dModelRegistry.mjs +309 -0
  61. package/src/ui/PcbScene3dPackages.mjs +137 -0
  62. package/src/ui/PcbScene3dScenePreparator.mjs +36 -0
  63. package/src/ui/PcbScene3dSummaryRenderer.mjs +65 -0
  64. package/src/ui/PcbSvgRenderer.mjs +906 -0
  65. package/src/ui/SchematicColorResolver.mjs +132 -0
  66. package/src/ui/SchematicContentLayout.mjs +661 -0
  67. package/src/ui/SchematicDirectiveRenderer.mjs +184 -0
  68. package/src/ui/SchematicImageRenderer.mjs +135 -0
  69. package/src/ui/SchematicJunctionRenderer.mjs +381 -0
  70. package/src/ui/SchematicNoteRenderer.mjs +427 -0
  71. package/src/ui/SchematicOwnerPinLabelLayout.mjs +173 -0
  72. package/src/ui/SchematicPinSvgRenderer.mjs +495 -0
  73. package/src/ui/SchematicPortRenderer.mjs +558 -0
  74. package/src/ui/SchematicPowerPortRenderer.mjs +574 -0
  75. package/src/ui/SchematicRegionRenderer.mjs +94 -0
  76. package/src/ui/SchematicShapeRenderer.mjs +398 -0
  77. package/src/ui/SchematicSheetChromeRenderer.mjs +1025 -0
  78. package/src/ui/SchematicSheetSymbolRenderer.mjs +228 -0
  79. package/src/ui/SchematicSvgRenderer.mjs +756 -0
  80. package/src/ui/SchematicSvgUtils.mjs +182 -0
  81. package/src/ui/SchematicTypography.mjs +204 -0
  82. package/src/workers/altium-parser.worker.mjs +29 -0
@@ -0,0 +1,495 @@
1
+ // SPDX-FileCopyrightText: 2026 André Fiedler
2
+ //
3
+ // SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ import { SchematicSvgUtils } from './SchematicSvgUtils.mjs'
6
+ import { SchematicTypography } from './SchematicTypography.mjs'
7
+ import { SchematicColorResolver } from './SchematicColorResolver.mjs'
8
+ import { SchematicOwnerPinLabelLayout } from './SchematicOwnerPinLabelLayout.mjs'
9
+
10
+ const { createSvgText, escapeHtml, formatNumber, projectSchematicY } =
11
+ SchematicSvgUtils
12
+
13
+ /**
14
+ * Renders one normalized schematic pin into SVG markup.
15
+ */
16
+ export class SchematicPinSvgRenderer {
17
+ /**
18
+ * Builds one schematic pin including its stub and visible labels.
19
+ * @param {{ x: number, y: number, length: number, name: string, nameSegments?: { text: string, overline: boolean }[], designator: string, orientation: 'left' | 'right' | 'top' | 'bottom', electrical?: number, symbolOuter?: number, color: string, labelColor?: string, labelMode?: 'hidden' | 'number-only' | 'name-only' | 'name-and-number', ownerIndex?: string }} pin
20
+ * @param {number} sheetHeight
21
+ * @param {{ fonts?: Record<string, { size: number, family: string, bold: boolean }> }} sheet
22
+ * @param {Set<string>} rotatedVerticalNumberOwners
23
+ * @param {Set<string>} explicitOwnerPinNameLabels
24
+ * @param {Map<string, number>} explicitOwnerPinLabelOffsets
25
+ * @returns {string}
26
+ */
27
+ static buildMarkup(
28
+ pin,
29
+ sheetHeight,
30
+ sheet,
31
+ rotatedVerticalNumberOwners,
32
+ explicitOwnerPinNameLabels,
33
+ explicitOwnerPinLabelOffsets
34
+ ) {
35
+ const geometry =
36
+ SchematicPinSvgRenderer.#projectSchematicPinGeometry(pin)
37
+ if (!geometry) return ''
38
+
39
+ const textOptions =
40
+ SchematicTypography.buildViewerSchematicFontOptions(sheet)
41
+ const projectedY = projectSchematicY(sheetHeight, pin.y)
42
+ const projectedInnerY = projectSchematicY(sheetHeight, geometry.bodyY)
43
+ const projectedOuterY = projectSchematicY(sheetHeight, geometry.outerY)
44
+ const texts = []
45
+ const labelColor = SchematicColorResolver.resolveColor(
46
+ pin.labelColor || pin.color,
47
+ '--schematic-text-color'
48
+ )
49
+ const labelMode = pin.labelMode || 'name-and-number'
50
+ const outerMarkerStyle =
51
+ SchematicPinSvgRenderer.#resolveSchematicOuterPinMarkerStyle(pin)
52
+ const usesOuterMarker = outerMarkerStyle !== null
53
+ const rotateTopNumber =
54
+ pin.orientation === 'top' &&
55
+ rotatedVerticalNumberOwners.has(String(pin.ownerIndex || ''))
56
+ const ownerPinLabelKey =
57
+ SchematicOwnerPinLabelLayout.buildOwnerPinLabelKey(
58
+ pin.ownerIndex,
59
+ pin.name
60
+ )
61
+ const hasExplicitOwnerPinName =
62
+ Boolean(pin.name) &&
63
+ explicitOwnerPinNameLabels.has(ownerPinLabelKey)
64
+
65
+ if (pin.orientation === 'left') {
66
+ if (labelMode !== 'hidden' && labelMode !== 'name-only') {
67
+ const defaultNumberX =
68
+ geometry.bodyX - (usesOuterMarker ? 8 : 2)
69
+ const numberX = hasExplicitOwnerPinName
70
+ ? SchematicOwnerPinLabelLayout.resolveExplicitOwnerPinNumberX(
71
+ pin,
72
+ defaultNumberX,
73
+ explicitOwnerPinLabelOffsets
74
+ )
75
+ : defaultNumberX
76
+ texts.push(
77
+ createSvgText(
78
+ 'schematic-pin-number',
79
+ numberX,
80
+ projectedY - 1,
81
+ pin.designator,
82
+ labelColor,
83
+ 'end',
84
+ textOptions
85
+ )
86
+ )
87
+ }
88
+
89
+ if (
90
+ labelMode !== 'hidden' &&
91
+ labelMode !== 'number-only' &&
92
+ pin.name &&
93
+ pin.name !== pin.designator &&
94
+ !hasExplicitOwnerPinName
95
+ ) {
96
+ texts.push(
97
+ SchematicPinSvgRenderer.#buildPinNameTextMarkup(
98
+ 'schematic-pin-name',
99
+ geometry.bodyX + (labelMode === 'name-only' ? 10 : 4),
100
+ projectedY + 3,
101
+ pin,
102
+ labelColor,
103
+ 'start',
104
+ textOptions
105
+ )
106
+ )
107
+ }
108
+ }
109
+
110
+ if (pin.orientation === 'right') {
111
+ if (labelMode !== 'hidden' && labelMode !== 'name-only') {
112
+ const defaultNumberX =
113
+ geometry.bodyX + (usesOuterMarker ? 8 : 2)
114
+ const numberX = hasExplicitOwnerPinName
115
+ ? SchematicOwnerPinLabelLayout.resolveExplicitOwnerPinNumberX(
116
+ pin,
117
+ defaultNumberX,
118
+ explicitOwnerPinLabelOffsets
119
+ )
120
+ : defaultNumberX
121
+ texts.push(
122
+ createSvgText(
123
+ 'schematic-pin-number',
124
+ numberX,
125
+ projectedY - 1,
126
+ pin.designator,
127
+ labelColor,
128
+ 'start',
129
+ textOptions
130
+ )
131
+ )
132
+ }
133
+
134
+ if (
135
+ labelMode !== 'hidden' &&
136
+ labelMode !== 'number-only' &&
137
+ pin.name &&
138
+ pin.name !== pin.designator &&
139
+ !hasExplicitOwnerPinName
140
+ ) {
141
+ texts.push(
142
+ SchematicPinSvgRenderer.#buildPinNameTextMarkup(
143
+ 'schematic-pin-name',
144
+ geometry.bodyX - (labelMode === 'name-only' ? 10 : 4),
145
+ projectedY + 3,
146
+ pin,
147
+ labelColor,
148
+ 'end',
149
+ textOptions
150
+ )
151
+ )
152
+ }
153
+ }
154
+
155
+ if (
156
+ labelMode !== 'hidden' &&
157
+ labelMode !== 'name-only' &&
158
+ (pin.orientation === 'top' || pin.orientation === 'bottom')
159
+ ) {
160
+ texts.push(
161
+ createSvgText(
162
+ 'schematic-pin-number',
163
+ geometry.bodyX - 2,
164
+ pin.orientation === 'top'
165
+ ? projectedInnerY - 6
166
+ : projectedInnerY + 7,
167
+ pin.designator,
168
+ labelColor,
169
+ 'middle',
170
+ pin.orientation === 'top' && !rotateTopNumber
171
+ ? textOptions
172
+ : { ...textOptions, rotation: -90 }
173
+ )
174
+ )
175
+ }
176
+
177
+ if (
178
+ labelMode !== 'hidden' &&
179
+ labelMode !== 'number-only' &&
180
+ pin.name &&
181
+ pin.name !== pin.designator &&
182
+ !hasExplicitOwnerPinName &&
183
+ (pin.orientation === 'top' || pin.orientation === 'bottom')
184
+ ) {
185
+ texts.push(
186
+ SchematicPinSvgRenderer.#buildPinNameTextMarkup(
187
+ 'schematic-pin-name',
188
+ pin.orientation === 'top'
189
+ ? geometry.bodyX
190
+ : geometry.bodyX + 4,
191
+ pin.orientation === 'top'
192
+ ? projectedInnerY + 4
193
+ : projectedInnerY - 4,
194
+ pin,
195
+ labelColor,
196
+ pin.orientation === 'top' ? 'end' : 'start',
197
+ { ...textOptions, rotation: -90 }
198
+ )
199
+ )
200
+ }
201
+
202
+ const markerMarkup = SchematicPinSvgRenderer.#buildPinMarkerMarkup(
203
+ pin,
204
+ geometry,
205
+ sheetHeight
206
+ )
207
+
208
+ return (
209
+ '<g class="schematic-pin"><line class="schematic-pin-line" x1="' +
210
+ formatNumber(geometry.bodyX) +
211
+ '" y1="' +
212
+ formatNumber(projectedInnerY) +
213
+ '" x2="' +
214
+ formatNumber(geometry.outerX) +
215
+ '" y2="' +
216
+ formatNumber(projectedOuterY) +
217
+ '" stroke="' +
218
+ escapeHtml(
219
+ SchematicColorResolver.resolveColor(
220
+ pin.color,
221
+ '--schematic-accent-ink-color'
222
+ )
223
+ ) +
224
+ '" />' +
225
+ markerMarkup +
226
+ texts.join('') +
227
+ '</g>'
228
+ )
229
+ }
230
+
231
+ /**
232
+ * Builds the authored or electrical pin marker for one horizontal pin.
233
+ * @param {{ electrical?: number, symbolOuter?: number, orientation: 'left' | 'right' | 'top' | 'bottom', labelColor?: string, color: string }} pin
234
+ * @param {{ bodyX: number, bodyY: number }} geometry
235
+ * @param {number} sheetHeight
236
+ * @returns {string}
237
+ */
238
+ static #buildPinMarkerMarkup(pin, geometry, sheetHeight) {
239
+ const outerMarkerStyle =
240
+ SchematicPinSvgRenderer.#resolveSchematicOuterPinMarkerStyle(pin)
241
+ if (outerMarkerStyle) {
242
+ return SchematicPinSvgRenderer.#buildOuterPinMarkerMarkup(
243
+ pin,
244
+ geometry,
245
+ sheetHeight,
246
+ outerMarkerStyle
247
+ )
248
+ }
249
+
250
+ if (Number(pin.electrical || 0) !== 1) {
251
+ return ''
252
+ }
253
+
254
+ if (pin.orientation !== 'left' && pin.orientation !== 'right') {
255
+ return ''
256
+ }
257
+
258
+ const direction = pin.orientation === 'left' ? 1 : -1
259
+ const bodyTipX = geometry.bodyX
260
+ const bodyBaseX = geometry.bodyX - direction * 5
261
+ const wireBaseX = geometry.bodyX - direction * 8
262
+ const wireTipX = geometry.bodyX - direction * 13
263
+ const halfHeight = 3
264
+ const projectedY = projectSchematicY(sheetHeight, geometry.bodyY)
265
+ const fillColor = SchematicColorResolver.resolveFill(
266
+ 'var(--schematic-pin-marker-fill)',
267
+ '--schematic-fill-light-color'
268
+ )
269
+ const strokeColor = SchematicColorResolver.resolveColor(
270
+ pin.labelColor || pin.color,
271
+ '--schematic-text-color'
272
+ )
273
+
274
+ return (
275
+ '<g class="schematic-pin-marker"><polygon points="' +
276
+ escapeHtml(
277
+ [
278
+ [bodyBaseX, projectedY - halfHeight],
279
+ [bodyBaseX, projectedY + halfHeight],
280
+ [bodyTipX, projectedY]
281
+ ]
282
+ .map(([x, y]) => formatNumber(x) + ',' + formatNumber(y))
283
+ .join(' ')
284
+ ) +
285
+ '" fill="' +
286
+ escapeHtml(fillColor) +
287
+ '" stroke="' +
288
+ escapeHtml(strokeColor) +
289
+ '" stroke-width="0.75" vector-effect="non-scaling-stroke" /><polygon points="' +
290
+ escapeHtml(
291
+ [
292
+ [wireBaseX, projectedY - halfHeight],
293
+ [wireBaseX, projectedY + halfHeight],
294
+ [wireTipX, projectedY]
295
+ ]
296
+ .map(([x, y]) => formatNumber(x) + ',' + formatNumber(y))
297
+ .join(' ')
298
+ ) +
299
+ '" fill="' +
300
+ escapeHtml(fillColor) +
301
+ '" stroke="' +
302
+ escapeHtml(strokeColor) +
303
+ '" stroke-width="0.75" vector-effect="non-scaling-stroke" /></g>'
304
+ )
305
+ }
306
+
307
+ /**
308
+ * Builds one authored outer pin glyph from the normalized marker style.
309
+ * @param {{ orientation: 'left' | 'right' | 'top' | 'bottom', labelColor?: string, color: string }} pin
310
+ * @param {{ bodyX: number, bodyY: number }} geometry
311
+ * @param {number} sheetHeight
312
+ * @param {'single-in' | 'single-out' | 'double'} markerStyle
313
+ * @returns {string}
314
+ */
315
+ static #buildOuterPinMarkerMarkup(pin, geometry, sheetHeight, markerStyle) {
316
+ const halfHeight = 3
317
+ const projectedY = projectSchematicY(sheetHeight, geometry.bodyY)
318
+ const fillColor = SchematicColorResolver.resolveFill(
319
+ 'var(--schematic-pin-marker-fill)',
320
+ '--schematic-fill-light-color'
321
+ )
322
+ const strokeColor = SchematicColorResolver.resolveColor(
323
+ pin.labelColor || pin.color,
324
+ '--schematic-text-color'
325
+ )
326
+
327
+ const polygons = SchematicPinSvgRenderer.#buildOuterPinMarkerPolygons(
328
+ geometry.bodyX,
329
+ projectedY,
330
+ pin.orientation,
331
+ halfHeight,
332
+ markerStyle
333
+ )
334
+
335
+ return (
336
+ '<g class="schematic-pin-marker">' +
337
+ polygons
338
+ .map(
339
+ (points) =>
340
+ '<polygon points="' +
341
+ escapeHtml(
342
+ points
343
+ .map(
344
+ ([x, y]) =>
345
+ formatNumber(x) + ',' + formatNumber(y)
346
+ )
347
+ .join(' ')
348
+ ) +
349
+ '" fill="' +
350
+ escapeHtml(fillColor) +
351
+ '" stroke="' +
352
+ escapeHtml(strokeColor) +
353
+ '" stroke-width="0.75" vector-effect="non-scaling-stroke" />'
354
+ )
355
+ .join('') +
356
+ '</g>'
357
+ )
358
+ }
359
+
360
+ /**
361
+ * Builds one or two authored outer-marker polygons for one horizontal pin.
362
+ * @param {number} bodyX
363
+ * @param {number} projectedY
364
+ * @param {'left' | 'right' | 'top' | 'bottom'} orientation
365
+ * @param {number} halfHeight
366
+ * @param {'single-in' | 'single-out' | 'double'} markerStyle
367
+ * @returns {number[][][]}
368
+ */
369
+ static #buildOuterPinMarkerPolygons(
370
+ bodyX,
371
+ projectedY,
372
+ orientation,
373
+ halfHeight,
374
+ markerStyle
375
+ ) {
376
+ const direction = orientation === 'left' ? 1 : -1
377
+ const inwardTriangle = [
378
+ [bodyX - direction * 6, projectedY - halfHeight],
379
+ [bodyX - direction * 6, projectedY + halfHeight],
380
+ [bodyX, projectedY]
381
+ ]
382
+ const outwardTriangle = [
383
+ [bodyX, projectedY - halfHeight],
384
+ [bodyX, projectedY + halfHeight],
385
+ [bodyX - direction * 6, projectedY]
386
+ ]
387
+
388
+ if (markerStyle === 'single-in') {
389
+ return [inwardTriangle]
390
+ }
391
+
392
+ if (markerStyle === 'single-out') {
393
+ return [outwardTriangle]
394
+ }
395
+
396
+ return [
397
+ inwardTriangle,
398
+ [
399
+ [bodyX - direction * 9, projectedY - halfHeight],
400
+ [bodyX - direction * 9, projectedY + halfHeight],
401
+ [bodyX - direction * 15, projectedY]
402
+ ]
403
+ ]
404
+ }
405
+
406
+ /**
407
+ * Resolves one authored outer pin marker style from the stored symbol flag.
408
+ * @param {{ symbolOuter?: number, orientation: 'left' | 'right' | 'top' | 'bottom' }} pin
409
+ * @returns {'single-in' | 'single-out' | 'double' | null}
410
+ */
411
+ static #resolveSchematicOuterPinMarkerStyle(pin) {
412
+ if (pin.orientation !== 'left' && pin.orientation !== 'right') {
413
+ return null
414
+ }
415
+
416
+ const symbolOuter = Number(pin.symbolOuter || 0)
417
+ switch (symbolOuter) {
418
+ case 1:
419
+ case 33:
420
+ return 'single-out'
421
+ case 2:
422
+ return 'single-in'
423
+ case 34:
424
+ return 'double'
425
+ default:
426
+ return null
427
+ }
428
+ }
429
+
430
+ /**
431
+ * Builds one pin-name text element, including overline tspans when needed.
432
+ * @param {string} className
433
+ * @param {number} x
434
+ * @param {number} y
435
+ * @param {{ name: string, nameSegments?: { text: string, overline: boolean }[] }} pin
436
+ * @param {string} color
437
+ * @param {'start' | 'end' | 'middle'} anchor
438
+ * @param {{ fontSize?: number, fontFamily?: string, fontWeight?: number, rotation?: number }} options
439
+ * @returns {string}
440
+ */
441
+ static #buildPinNameTextMarkup(
442
+ className,
443
+ x,
444
+ y,
445
+ pin,
446
+ color,
447
+ anchor,
448
+ options
449
+ ) {
450
+ return createSvgText(className, x, y, pin.name, color, anchor, {
451
+ ...options,
452
+ segments: pin.nameSegments
453
+ })
454
+ }
455
+
456
+ /**
457
+ * Computes the inner endpoint for a schematic pin stub.
458
+ * @param {{ x: number, y: number, length: number, orientation: 'left' | 'right' | 'top' | 'bottom' }} pin
459
+ * @returns {{ bodyX: number, bodyY: number, outerX: number, outerY: number } | null}
460
+ */
461
+ static #projectSchematicPinGeometry(pin) {
462
+ switch (pin.orientation) {
463
+ case 'left':
464
+ return {
465
+ bodyX: pin.x,
466
+ bodyY: pin.y,
467
+ outerX: pin.x - pin.length,
468
+ outerY: pin.y
469
+ }
470
+ case 'right':
471
+ return {
472
+ bodyX: pin.x,
473
+ bodyY: pin.y,
474
+ outerX: pin.x + pin.length,
475
+ outerY: pin.y
476
+ }
477
+ case 'top':
478
+ return {
479
+ bodyX: pin.x,
480
+ bodyY: pin.y,
481
+ outerX: pin.x,
482
+ outerY: pin.y + pin.length
483
+ }
484
+ case 'bottom':
485
+ return {
486
+ bodyX: pin.x,
487
+ bodyY: pin.y,
488
+ outerX: pin.x,
489
+ outerY: pin.y - pin.length
490
+ }
491
+ default:
492
+ return null
493
+ }
494
+ }
495
+ }