altium-toolkit 0.1.0 → 0.1.16

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 (54) hide show
  1. package/README.md +24 -6
  2. package/docs/api.md +42 -4
  3. package/docs/model-format.md +95 -5
  4. package/docs/schemas/altium_toolkit/normalized_model_a1.schema.json +553 -0
  5. package/docs/testing.md +7 -2
  6. package/package.json +21 -2
  7. package/spec/library-scope.md +7 -1
  8. package/src/core/altium/AltiumParser.mjs +22 -325
  9. package/src/core/altium/NormalizedModelSchema.mjs +28 -0
  10. package/src/core/altium/PcbArcPrimitiveParser.mjs +87 -0
  11. package/src/core/altium/PcbBinaryPrimitiveParser.mjs +43 -370
  12. package/src/core/altium/PcbBoardRegionSemanticsParser.mjs +477 -0
  13. package/src/core/altium/PcbComponentAnnotationNormalizer.mjs +290 -0
  14. package/src/core/altium/PcbComponentBodyPlacementNormalizer.mjs +52 -0
  15. package/src/core/altium/PcbComponentPrimitiveIndexer.mjs +109 -0
  16. package/src/core/altium/PcbEmbeddedFontExtractor.mjs +484 -0
  17. package/src/core/altium/PcbFillPrimitiveParser.mjs +84 -0
  18. package/src/core/altium/PcbFontMetricsParser.mjs +308 -0
  19. package/src/core/altium/PcbGeometryFlipper.mjs +244 -0
  20. package/src/core/altium/PcbLayerIdCodec.mjs +136 -0
  21. package/src/core/altium/PcbLibModelParser.mjs +202 -0
  22. package/src/core/altium/PcbLibStreamExtractor.mjs +968 -0
  23. package/src/core/altium/PcbModelParser.mjs +618 -66
  24. package/src/core/altium/PcbOutlineRecovery.mjs +4 -112
  25. package/src/core/altium/PcbPadPrimitiveParser.mjs +347 -0
  26. package/src/core/altium/PcbPadShapeCodec.mjs +158 -0
  27. package/src/core/altium/PcbPadStackParser.mjs +903 -0
  28. package/src/core/altium/PcbPrimitiveOwnershipIndexParser.mjs +60 -0
  29. package/src/core/altium/PcbPrimitiveParameterParser.mjs +212 -0
  30. package/src/core/altium/PcbPrimitiveRecordSlicer.mjs +243 -0
  31. package/src/core/altium/PcbRawRecordRegistry.mjs +831 -0
  32. package/src/core/altium/PcbRegionPrimitiveParser.mjs +317 -0
  33. package/src/core/altium/PcbRuleParser.mjs +587 -0
  34. package/src/core/altium/PcbStreamExtractor.mjs +127 -4
  35. package/src/core/altium/PcbTextPrimitiveParser.mjs +537 -0
  36. package/src/core/altium/PcbTrackPrimitiveParser.mjs +87 -0
  37. package/src/core/altium/PcbViaPrimitiveParser.mjs +88 -0
  38. package/src/core/altium/PcbViaStackParser.mjs +548 -0
  39. package/src/core/altium/PcbWideStringTableParser.mjs +108 -0
  40. package/src/core/altium/PrjPcbModelParser.mjs +797 -0
  41. package/src/core/altium/SchematicComponentTextResolver.mjs +355 -0
  42. package/src/parser.mjs +13 -0
  43. package/src/renderers.mjs +5 -0
  44. package/src/styles/altium-renderers.css +11 -6
  45. package/src/ui/PcbCopperPrimitiveSplitter.mjs +113 -0
  46. package/src/ui/PcbEdgeFacingGlyphNormalizer.mjs +6 -5
  47. package/src/ui/PcbEmbeddedFontFaceRenderer.mjs +126 -0
  48. package/src/ui/PcbFootprintPrimitiveSelector.mjs +27 -6
  49. package/src/ui/PcbRegionPrimitiveRenderer.mjs +243 -0
  50. package/src/ui/PcbSideResolvedRenderModel.mjs +336 -0
  51. package/src/ui/PcbSvgRenderer.mjs +101 -109
  52. package/src/ui/PcbTextPrimitiveRenderer.mjs +252 -0
  53. package/src/ui/SchematicSheetChromeRenderer.mjs +2 -93
  54. package/src/ui/SchematicSheetZoneRenderer.mjs +104 -0
@@ -12,7 +12,11 @@ import { SchematicDirectiveParser } from './SchematicDirectiveParser.mjs'
12
12
  import { SchematicPinParser } from './SchematicPinParser.mjs'
13
13
  import { SchematicPrimitiveParser } from './SchematicPrimitiveParser.mjs'
14
14
  import { AltiumLayoutParser } from './AltiumLayoutParser.mjs'
15
+ import { NormalizedModelSchema } from './NormalizedModelSchema.mjs'
15
16
  import { PcbModelParser } from './PcbModelParser.mjs'
17
+ import { PcbLibModelParser } from './PcbLibModelParser.mjs'
18
+ import { PcbLibStreamExtractor } from './PcbLibStreamExtractor.mjs'
19
+ import { PrjPcbModelParser } from './PrjPcbModelParser.mjs'
16
20
  import { PcbStreamExtractor } from './PcbStreamExtractor.mjs'
17
21
  import { SchematicMultipartOwnerMatcher } from './SchematicMultipartOwnerMatcher.mjs'
18
22
  import { SchematicSheetStyleResolver } from './SchematicSheetStyleResolver.mjs'
@@ -21,6 +25,7 @@ import { SchematicJunctionParser } from './SchematicJunctionParser.mjs'
21
25
  import { SchematicBusEntryParser } from './SchematicBusEntryParser.mjs'
22
26
  import { SchematicImageParser } from './SchematicImageParser.mjs'
23
27
  import { SchematicNetlistBuilder } from './SchematicNetlistBuilder.mjs'
28
+ import { SchematicComponentTextResolver } from './SchematicComponentTextResolver.mjs'
24
29
  const {
25
30
  countMatchingKeys,
26
31
  getDisplayText,
@@ -54,7 +59,7 @@ export class AltiumParser {
54
59
  * Parses a native Altium buffer into a normalized viewer model.
55
60
  * @param {string} fileName
56
61
  * @param {ArrayBuffer} arrayBuffer
57
- * @returns {{ kind: 'schematic' | 'pcb', fileType: 'SchDoc' | 'PcbDoc', fileName: string, summary: Record<string, number | string>, diagnostics: { severity: 'info' | 'warning', message: string }[], schematic?: Record<string, unknown>, pcb?: Record<string, unknown>, bom: { designators: string[], quantity: number, pattern: string, source: string, value: string }[] }}
62
+ * @returns {{ schema: string, kind: 'schematic' | 'pcb' | 'pcb-library' | 'project', fileType: 'SchDoc' | 'PcbDoc' | 'PcbLib' | 'PrjPcb', fileName: string, summary: Record<string, number | string>, diagnostics: { severity: 'info' | 'warning', message: string }[], schematic?: Record<string, unknown>, pcb?: Record<string, unknown>, pcbLibrary?: Record<string, unknown>, project?: Record<string, unknown>, bom: { designators: string[], quantity: number, pattern: string, source: string, value: string }[] }}
58
63
  */
59
64
  static parseArrayBuffer(fileName, arrayBuffer) {
60
65
  const records = AsciiRecordParser.parse(arrayBuffer)
@@ -71,6 +76,15 @@ export class AltiumParser {
71
76
  pcbExtraction
72
77
  )
73
78
  }
79
+ if (fileType === 'PcbLib') {
80
+ return PcbLibModelParser.parse(
81
+ fileName,
82
+ PcbLibStreamExtractor.extractFromArrayBuffer(arrayBuffer)
83
+ )
84
+ }
85
+ if (fileType === 'PrjPcb') {
86
+ return PrjPcbModelParser.parse(fileName, arrayBuffer)
87
+ }
74
88
  throw new Error('Unsupported file type: ' + fileName)
75
89
  }
76
90
 
@@ -78,12 +92,14 @@ export class AltiumParser {
78
92
  * Chooses the format based on extension and content.
79
93
  * @param {string} fileName
80
94
  * @param {{ fields: Record<string, string | string[]> }[]} records
81
- * @returns {'SchDoc' | 'PcbDoc'}
95
+ * @returns {'SchDoc' | 'PcbDoc' | 'PcbLib' | 'PrjPcb'}
82
96
  */
83
97
  static #sniffFileType(fileName, records) {
84
98
  const normalized = String(fileName || '').toLowerCase()
85
99
  if (normalized.endsWith('.schdoc')) return 'SchDoc'
86
100
  if (normalized.endsWith('.pcbdoc')) return 'PcbDoc'
101
+ if (normalized.endsWith('.pcblib')) return 'PcbLib'
102
+ if (normalized.endsWith('.prjpcb')) return 'PrjPcb'
87
103
 
88
104
  const hasSchematicHeader = records.some((record) =>
89
105
  getField(record.fields, 'HEADER').includes('Schematic')
@@ -386,7 +402,7 @@ export class AltiumParser {
386
402
  y,
387
403
  libReference,
388
404
  designator:
389
- AltiumParser.#resolveComponentDesignator(
405
+ SchematicComponentTextResolver.resolveDesignator(
390
406
  ownerTexts,
391
407
  anchoredTexts,
392
408
  {
@@ -395,7 +411,7 @@ export class AltiumParser {
395
411
  libReference
396
412
  }
397
413
  ) || 'U?',
398
- value: AltiumParser.#resolveComponentValue(
414
+ value: SchematicComponentTextResolver.resolveValue(
399
415
  ownerTexts,
400
416
  anchoredTexts,
401
417
  { x, y, libReference }
@@ -474,7 +490,7 @@ export class AltiumParser {
474
490
  })
475
491
  diagnostics.push(...netDiagnostics)
476
492
 
477
- return {
493
+ return NormalizedModelSchema.attach({
478
494
  kind: 'schematic',
479
495
  fileType: 'SchDoc',
480
496
  fileName,
@@ -511,7 +527,7 @@ export class AltiumParser {
511
527
  nets
512
528
  },
513
529
  bom
514
- }
530
+ })
515
531
  }
516
532
 
517
533
  /**
@@ -529,21 +545,6 @@ export class AltiumParser {
529
545
  return match ? getDisplayText(match.fields) : ''
530
546
  }
531
547
 
532
- /**
533
- * Finds a related text value by name.
534
- * @param {{ fields: Record<string, string | string[]> }[]} records
535
- * @param {string} logicalName
536
- * @returns {string}
537
- */
538
- static #findRelatedText(records, logicalName) {
539
- const match = records.find(
540
- (record) =>
541
- getField(record.fields, 'Name').toLowerCase() ===
542
- logicalName.toLowerCase()
543
- )
544
- return match ? getDisplayText(match.fields) : ''
545
- }
546
-
547
548
  /**
548
549
  * Collects owners whose active symbol primitives already exist without an
549
550
  * explicit display-mode selector.
@@ -636,310 +637,6 @@ export class AltiumParser {
636
637
  )
637
638
  }
638
639
 
639
- /**
640
- * Resolves a component designator from owner-linked text or nearby visible
641
- * schematic labels when the owner link is missing.
642
- * @param {{ fields: Record<string, string | string[]> }[]} ownerTexts
643
- * @param {{ x: number, y: number, text: string, name: string }[]} texts
644
- * @param {{ x: number, y: number, libReference: string }} component
645
- * @returns {string}
646
- */
647
- static #resolveComponentDesignator(ownerTexts, texts, component) {
648
- const ownerDesignator = AltiumParser.#findRelatedText(
649
- ownerTexts,
650
- 'Designator'
651
- )
652
- if (AltiumParser.#isResolvedComponentText(ownerDesignator)) {
653
- return ownerDesignator
654
- }
655
-
656
- return AltiumParser.#findNearbyComponentDesignator(texts, component)
657
- }
658
-
659
- /**
660
- * Resolves a component value from owner-linked text or nearby visible
661
- * schematic labels when the owner link still contains template placeholders.
662
- * @param {{ fields: Record<string, string | string[]> }[]} ownerTexts
663
- * @param {{ x: number, y: number, text: string, name: string }[]} texts
664
- * @param {{ x: number, y: number, libReference: string }} component
665
- * @returns {string}
666
- */
667
- static #resolveComponentValue(ownerTexts, texts, component) {
668
- const ownerValue =
669
- AltiumParser.#findRelatedText(ownerTexts, 'Comment') ||
670
- AltiumParser.#findRelatedText(ownerTexts, 'VALUE')
671
- if (AltiumParser.#isResolvedComponentText(ownerValue)) {
672
- return ownerValue
673
- }
674
-
675
- return (
676
- AltiumParser.#findNearbyComponentText(
677
- texts,
678
- component,
679
- ['comment', 'value'],
680
- '',
681
- AltiumParser.#inferComponentValueHint(component.libReference)
682
- ) || ownerValue
683
- )
684
- }
685
-
686
- /**
687
- * Finds the closest nearby designator text for one component.
688
- * @param {{ x: number, y: number, text: string, name: string }[]} texts
689
- * @param {{ x: number, y: number, libReference: string }} component
690
- * @returns {string}
691
- */
692
- static #findNearbyComponentDesignator(texts, component) {
693
- const expectedPrefix = AltiumParser.#inferComponentDesignatorPrefix(
694
- component.libReference
695
- )
696
- const expectedValueHint = AltiumParser.#inferComponentValueHint(
697
- component.libReference
698
- )
699
- const candidates = AltiumParser.#collectNearbyComponentTextCandidates(
700
- texts,
701
- component,
702
- ['designator']
703
- )
704
- const scopedCandidates = expectedPrefix
705
- ? candidates.filter((candidate) =>
706
- candidate.text
707
- .toUpperCase()
708
- .startsWith(expectedPrefix.toUpperCase())
709
- )
710
- : candidates
711
- const usableCandidates = scopedCandidates.length
712
- ? scopedCandidates
713
- : candidates
714
- const rankedCandidates = usableCandidates
715
- .map((candidate) => ({
716
- ...candidate,
717
- score:
718
- candidate.distance +
719
- AltiumParser.#scoreAssociatedValueMismatch(
720
- texts,
721
- candidate,
722
- expectedValueHint
723
- )
724
- }))
725
- .sort((left, right) => left.score - right.score)
726
-
727
- return rankedCandidates[0]?.text || ''
728
- }
729
-
730
- /**
731
- * Finds the closest nearby visible text for one component.
732
- * @param {{ x: number, y: number, text: string, name: string }[]} texts
733
- * @param {{ x: number, y: number }} component
734
- * @param {string[]} logicalNames
735
- * @param {string} expectedPrefix
736
- * @param {string} expectedTextHint
737
- * @returns {string}
738
- */
739
- static #findNearbyComponentText(
740
- texts,
741
- component,
742
- logicalNames,
743
- expectedPrefix = '',
744
- expectedTextHint = ''
745
- ) {
746
- const candidates = AltiumParser.#collectNearbyComponentTextCandidates(
747
- texts,
748
- component,
749
- logicalNames
750
- )
751
- const prefixedCandidates = expectedPrefix
752
- ? candidates.filter((candidate) =>
753
- candidate.text
754
- .toUpperCase()
755
- .startsWith(expectedPrefix.toUpperCase())
756
- )
757
- : candidates
758
- const scopedCandidates = prefixedCandidates.length
759
- ? prefixedCandidates
760
- : candidates
761
- const hintedCandidates = expectedTextHint
762
- ? scopedCandidates.filter((candidate) =>
763
- AltiumParser.#normalizeTextMatch(candidate.text).includes(
764
- AltiumParser.#normalizeTextMatch(expectedTextHint)
765
- )
766
- )
767
- : scopedCandidates
768
- const usableCandidates = hintedCandidates.length
769
- ? hintedCandidates
770
- : scopedCandidates
771
-
772
- return usableCandidates.sort(
773
- (left, right) => left.distance - right.distance
774
- )[0]?.text
775
- }
776
-
777
- /**
778
- * Collects nearby visible schematic text candidates around one component.
779
- * @param {{ x: number, y: number, text: string, name: string }[]} texts
780
- * @param {{ x: number, y: number }} component
781
- * @param {string[]} logicalNames
782
- * @returns {{ x: number, y: number, text: string, distance: number }[]}
783
- */
784
- static #collectNearbyComponentTextCandidates(
785
- texts,
786
- component,
787
- logicalNames
788
- ) {
789
- const allowedNames = new Set(
790
- logicalNames.map((name) => name.toLowerCase())
791
- )
792
-
793
- return texts
794
- .filter((text) =>
795
- allowedNames.has(
796
- String(text.name || '')
797
- .trim()
798
- .toLowerCase()
799
- )
800
- )
801
- .map((text) => ({
802
- x: text.x,
803
- y: text.y,
804
- text: text.text,
805
- distance:
806
- Math.abs(text.x - component.x) +
807
- Math.abs(text.y - component.y)
808
- }))
809
- .filter(
810
- (text) =>
811
- Math.abs(text.x - component.x) <= 80 &&
812
- Math.abs(text.y - component.y) <= 80
813
- )
814
- }
815
-
816
- /**
817
- * Penalizes a designator candidate when its nearby value text does not
818
- * match the library-derived value hint.
819
- * @param {{ x: number, y: number, text: string, name: string }[]} texts
820
- * @param {{ x: number, y: number }} candidate
821
- * @param {string} expectedValueHint
822
- * @returns {number}
823
- */
824
- static #scoreAssociatedValueMismatch(texts, candidate, expectedValueHint) {
825
- if (!expectedValueHint) {
826
- return 0
827
- }
828
-
829
- const associatedValue = AltiumParser.#findNearbyComponentText(
830
- texts,
831
- candidate,
832
- ['comment', 'value']
833
- )
834
- if (!associatedValue) {
835
- return 0
836
- }
837
-
838
- return AltiumParser.#normalizeTextMatch(associatedValue).includes(
839
- AltiumParser.#normalizeTextMatch(expectedValueHint)
840
- )
841
- ? -30
842
- : 30
843
- }
844
-
845
- /**
846
- * Returns true when a recovered owner-linked text is usable as a component
847
- * display value.
848
- * @param {string} value
849
- * @returns {boolean}
850
- */
851
- static #isResolvedComponentText(value) {
852
- const normalized = String(value || '').trim()
853
-
854
- return Boolean(
855
- normalized && normalized !== '*' && !normalized.startsWith('=')
856
- )
857
- }
858
-
859
- /**
860
- * Infers the visible designator prefix from a library reference.
861
- * @param {string} libReference
862
- * @returns {string}
863
- */
864
- static #inferComponentDesignatorPrefix(libReference) {
865
- const normalized = String(libReference || '')
866
- .trim()
867
- .toUpperCase()
868
-
869
- if (normalized.startsWith('RES/')) return 'R'
870
- if (normalized.startsWith('CAP/')) return 'C'
871
- if (normalized.startsWith('DIODE/')) return 'D'
872
- if (normalized.startsWith('CON/')) return 'J'
873
- if (normalized.startsWith('IC/')) return 'U'
874
-
875
- return ''
876
- }
877
-
878
- /**
879
- * Infers the visible value label from a library reference.
880
- * @param {string} libReference
881
- * @returns {string}
882
- */
883
- static #inferComponentValueHint(libReference) {
884
- const segments = String(libReference || '')
885
- .split('/')
886
- .map((segment) => segment.trim())
887
- .filter(Boolean)
888
-
889
- for (let index = segments.length - 1; index >= 0; index -= 1) {
890
- const segment = segments[index]
891
-
892
- if (
893
- AltiumParser.#isPackageLikeComponentSegment(segment) ||
894
- /\s/.test(segment)
895
- ) {
896
- continue
897
- }
898
-
899
- if (
900
- /^(?:\d+(?:\.\d+)?(?:R|K|M|UF|NF|PF)|1N[A-Z0-9-]+)$/i.test(
901
- segment
902
- )
903
- ) {
904
- return segment
905
- }
906
-
907
- if (
908
- /[A-Z]/i.test(segment) &&
909
- /\d/.test(segment) &&
910
- segment.length >= 6
911
- ) {
912
- return segment
913
- }
914
- }
915
-
916
- return ''
917
- }
918
-
919
- /**
920
- * Returns true when one library segment behaves like a package or rating
921
- * rather than a user-facing value.
922
- * @param {string} segment
923
- * @returns {boolean}
924
- */
925
- static #isPackageLikeComponentSegment(segment) {
926
- return /^(?:CE|\d{4}|SC\d+|SOD-\d+|\d+(?:\.\d+)?V|\d+(?:\.\d+)?[%%])$/i.test(
927
- String(segment || '').trim()
928
- )
929
- }
930
-
931
- /**
932
- * Normalizes a text fragment for proximity matching.
933
- * @param {string} value
934
- * @returns {string}
935
- */
936
- static #normalizeTextMatch(value) {
937
- return String(value || '')
938
- .toUpperCase()
939
- .replaceAll(/\s+/g, '')
940
- .replaceAll('%', '%')
941
- }
942
-
943
640
  /**
944
641
  * Groups designators into BOM rows.
945
642
  * @param {{ designator: string, pattern: string, source: string, value: string }[]} entries
@@ -0,0 +1,28 @@
1
+ // SPDX-FileCopyrightText: 2026 André Fiedler
2
+ //
3
+ // SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ /**
6
+ * Defines the current normalized model contract emitted by parser roots.
7
+ */
8
+ export class NormalizedModelSchema {
9
+ static CURRENT_SCHEMA_ID = 'urn:altium-toolkit:normalized-model:a1'
10
+
11
+ static CURRENT_SCHEMA_VERSION = 'a1'
12
+
13
+ /**
14
+ * Adds the current normalized model schema id to a parser root object.
15
+ * @template {Record<string, unknown>} T
16
+ * @param {T} model
17
+ * @returns {T & { schema: string }}
18
+ */
19
+ static attach(model) {
20
+ const normalizedModel = {
21
+ schema: NormalizedModelSchema.CURRENT_SCHEMA_ID,
22
+ ...model
23
+ }
24
+ normalizedModel.schema = NormalizedModelSchema.CURRENT_SCHEMA_ID
25
+
26
+ return normalizedModel
27
+ }
28
+ }
@@ -0,0 +1,87 @@
1
+ // SPDX-FileCopyrightText: 2026 André Fiedler
2
+ //
3
+ // SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ import { PcbPrimitiveRecordSlicer } from './PcbPrimitiveRecordSlicer.mjs'
6
+ import { PcbPrimitiveOwnershipIndexParser } from './PcbPrimitiveOwnershipIndexParser.mjs'
7
+
8
+ /**
9
+ * Decodes Altium arc primitive streams.
10
+ */
11
+ export class PcbArcPrimitiveParser {
12
+ static #ARC_OBJECT_ID = 1
13
+
14
+ static #ARC_RECORD_BYTE_LENGTH = 60
15
+
16
+ static #ARC_PAYLOAD_MIN_BYTE_LENGTH = 45
17
+
18
+ /**
19
+ * Decodes one arc stream.
20
+ * @param {Uint8Array | ArrayBuffer} headerBytes
21
+ * @param {Uint8Array | ArrayBuffer} dataBytes
22
+ * @returns {{ x: number, y: number, radius: number, startAngle: number, endAngle: number, width: number, componentIndex: number | null, netIndex: number | null, polygonIndex: number | null, layerCode: number, layerId: number }[]}
23
+ */
24
+ static parseArcStream(headerBytes, dataBytes) {
25
+ return PcbArcPrimitiveParser.#sliceArcRecords(
26
+ headerBytes,
27
+ dataBytes
28
+ ).map((view) => PcbArcPrimitiveParser.#parseArcRecord(view))
29
+ }
30
+
31
+ /**
32
+ * Decodes one arc record view into a normalized primitive.
33
+ * @param {DataView} view
34
+ * @returns {{ x: number, y: number, radius: number, startAngle: number, endAngle: number, width: number, componentIndex: number | null, netIndex: number | null, polygonIndex: number | null, layerCode: number, layerId: number }}
35
+ */
36
+ static #parseArcRecord(view) {
37
+ const layerId = view.getUint8(0)
38
+ const ownershipIndexes =
39
+ PcbPrimitiveOwnershipIndexParser.readOwnershipIndexes(view, {
40
+ component: 7,
41
+ net: 3,
42
+ polygon: 5
43
+ })
44
+
45
+ return {
46
+ x: PcbArcPrimitiveParser.#readMil(view, 13),
47
+ y: PcbArcPrimitiveParser.#readMil(view, 17),
48
+ radius: PcbArcPrimitiveParser.#readMil(view, 21),
49
+ startAngle: view.getFloat64(25, true),
50
+ endAngle: view.getFloat64(33, true),
51
+ width: PcbArcPrimitiveParser.#readMil(view, 41),
52
+ ...ownershipIndexes,
53
+ layerCode: layerId,
54
+ layerId
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Splits an arc stream into record views, preserving variable payload
60
+ * lengths when arcs are stored with object-id and payload-length prefixes.
61
+ * @param {Uint8Array | ArrayBuffer} headerBytes
62
+ * @param {Uint8Array | ArrayBuffer} dataBytes
63
+ * @returns {DataView[]}
64
+ */
65
+ static #sliceArcRecords(headerBytes, dataBytes) {
66
+ return PcbPrimitiveRecordSlicer.slicePrimitiveRecords({
67
+ headerBytes,
68
+ dataBytes,
69
+ objectId: PcbArcPrimitiveParser.#ARC_OBJECT_ID,
70
+ fixedRecordByteLength:
71
+ PcbArcPrimitiveParser.#ARC_RECORD_BYTE_LENGTH,
72
+ minimumPayloadByteLength:
73
+ PcbArcPrimitiveParser.#ARC_PAYLOAD_MIN_BYTE_LENGTH,
74
+ lengthPrefixedView: 'payload'
75
+ }).map((record) => record.view)
76
+ }
77
+
78
+ /**
79
+ * Reads one standard fixed-point mil value.
80
+ * @param {DataView} view
81
+ * @param {number} offset
82
+ * @returns {number}
83
+ */
84
+ static #readMil(view, offset) {
85
+ return view.getInt32(offset, true) / 10000
86
+ }
87
+ }