@vessel-dsp/core 0.5.0 → 0.6.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.
@@ -3,8 +3,32 @@ import type {
3
3
  CircuitDocument,
4
4
  CircuitDocumentDevice,
5
5
  CircuitDocumentDeviceKind,
6
+ BoardApplicability,
7
+ BoardEdgeTerminal,
8
+ BoardFamily,
9
+ BoardFootprint,
10
+ BoardFootprintCatalog,
11
+ BoardFootprintPlacement,
12
+ BoardKind,
13
+ BoardNet,
14
+ BoardNetMember,
15
+ BoardNetlist,
16
+ BoardPlacedPad,
17
+ BoardRealization,
18
+ BoardRoute,
19
+ BoardSourceCircuitHash,
20
+ BoardSubtype,
21
+ BuildBom,
22
+ BuildBomItem,
23
+ BuildBomRef,
24
+ BuildCompleteness,
25
+ BuildIntent,
26
+ BuildPartProfile,
27
+ BuildPartProfileCatalog,
28
+ BuildScope,
6
29
  Component,
7
30
  ComponentKind,
31
+ ComponentTerminalRef,
8
32
  ControlApplicabilityPredicate,
9
33
  ControlContext,
10
34
  ControlGroup,
@@ -22,16 +46,27 @@ import type {
22
46
  PanelColumnOrder,
23
47
  PanelControlKind,
24
48
  PanelElementBinding,
49
+ PanelElementPhysicalPlacement,
50
+ PanelFaceGeometry,
25
51
  PanelGridLayout,
26
52
  PanelGridIndexing,
27
53
  PanelGridPosition,
28
54
  PanelPlacementMetadata,
29
55
  PanelRowOrder,
56
+ MechanicalBuildMetadata,
57
+ OffBoardWiringConnection,
58
+ OffBoardWiringCoverage,
59
+ OffBoardWiringEndpoint,
60
+ OffBoardWiringHarness,
61
+ OffBoardWiringHarnessStatus,
62
+ OffBoardWiringPlan,
30
63
  ParsedQuantity,
31
64
  Point,
32
65
  PropertyValue,
33
66
  Rotation,
34
67
  Terminal,
68
+ VdspBuildDataObject,
69
+ VdspBuildDataValue,
35
70
  Warning,
36
71
  Wire,
37
72
  } from '../../model/types';
@@ -55,23 +90,44 @@ type ParsedPair = Readonly<{
55
90
  rest: string;
56
91
  }>;
57
92
 
58
- const INTERCHANGE_SCHEMA = 'circuit-interchange/v2';
93
+ const INTERCHANGE_SCHEMA_V2 = 'circuit-interchange/v2';
94
+ const INTERCHANGE_SCHEMA_V3 = 'circuit-interchange/v3';
95
+ const V3_ONLY_TOP_LEVEL_FIELDS = [
96
+ 'mechanical',
97
+ 'build',
98
+ 'bom',
99
+ 'partProfiles',
100
+ 'footprints',
101
+ 'offBoardWiring',
102
+ 'boards',
103
+ ] as const;
59
104
 
60
105
  export function parseInterchangeYaml(source: string): CircuitDocument {
61
106
  const value = parseYamlSubset(source);
62
107
  const root = expectObject(value, 'root');
63
108
  const schema = expectString(root.schema, 'schema');
64
- if (schema !== INTERCHANGE_SCHEMA) {
109
+ if (schema !== INTERCHANGE_SCHEMA_V2 && schema !== INTERCHANGE_SCHEMA_V3) {
65
110
  throw new Error(`unsupported interchange schema: ${schema}`);
66
111
  }
112
+ const isV3 = schema === INTERCHANGE_SCHEMA_V3;
113
+ if (!isV3) {
114
+ rejectV3OnlyTopLevelFields(root);
115
+ }
67
116
 
68
- const panel = parsePanel(root.panel);
117
+ const panel = parsePanel(root.panel, isV3);
69
118
  const controlInterfaces = parseControlInterfaces(root.controlInterfaces);
70
119
  const device = parseDevice(root.device);
71
120
  const controlOutputs = parseControlOutputs(root.controlOutputs);
72
121
  const controlGroups = parseControlGroups(root.controlGroups);
73
122
  const controlContexts = parseControlContexts(root.controlContexts);
74
123
  const deviceInterface = parseDeviceInterface(root.deviceInterface);
124
+ const mechanical = isV3 ? parseMechanical(root.mechanical) : undefined;
125
+ const build = isV3 ? parseBuild(root.build) : undefined;
126
+ const bom = isV3 ? parseBom(root.bom) : undefined;
127
+ const partProfiles = isV3 ? parsePartProfiles(root.partProfiles) : undefined;
128
+ const footprints = isV3 ? parseFootprints(root.footprints) : undefined;
129
+ const offBoardWiring = isV3 ? parseOffBoardWiring(root.offBoardWiring) : undefined;
130
+ const boards = isV3 ? parseBoards(root.boards) : undefined;
75
131
 
76
132
  return {
77
133
  metadata: parseMetadata(root.metadata),
@@ -79,6 +135,13 @@ export function parseInterchangeYaml(source: string): CircuitDocument {
79
135
  ...(device === undefined ? {} : { device }),
80
136
  ...(controlGroups === undefined ? {} : { controlGroups }),
81
137
  ...(controlContexts === undefined ? {} : { controlContexts }),
138
+ ...(mechanical === undefined ? {} : { mechanical }),
139
+ ...(build === undefined ? {} : { build }),
140
+ ...(bom === undefined ? {} : { bom }),
141
+ ...(partProfiles === undefined ? {} : { partProfiles }),
142
+ ...(footprints === undefined ? {} : { footprints }),
143
+ ...(offBoardWiring === undefined ? {} : { offBoardWiring }),
144
+ ...(boards === undefined ? {} : { boards }),
82
145
  ...(deviceInterface === undefined ? {} : { deviceInterface }),
83
146
  ...(panel === undefined ? {} : { panel }),
84
147
  ...(controlInterfaces === undefined ? {} : { controlInterfaces }),
@@ -91,6 +154,525 @@ export function parseInterchangeYaml(source: string): CircuitDocument {
91
154
  };
92
155
  }
93
156
 
157
+ function rejectV3OnlyTopLevelFields(root: YamlObject): void {
158
+ for (const field of V3_ONLY_TOP_LEVEL_FIELDS) {
159
+ if (root[field] !== undefined) {
160
+ throw new Error(`${field}: requires schema ${INTERCHANGE_SCHEMA_V3}`);
161
+ }
162
+ }
163
+ }
164
+
165
+ function parseMechanical(value: YamlValue | undefined): MechanicalBuildMetadata | undefined {
166
+ if (value === undefined) {
167
+ return undefined;
168
+ }
169
+ const mechanical = expectObject(value, 'mechanical');
170
+ return {
171
+ ...parseBuildDataObject(mechanical, 'mechanical'),
172
+ ...(mechanical.schema === undefined ? {} : { schema: expectString(mechanical.schema, 'mechanical.schema') }),
173
+ ...(mechanical.units === undefined ? {} : { units: expectString(mechanical.units, 'mechanical.units') }),
174
+ };
175
+ }
176
+
177
+ function parseBuild(value: YamlValue | undefined): BuildScope | undefined {
178
+ if (value === undefined) {
179
+ return undefined;
180
+ }
181
+ const build = expectObject(value, 'build');
182
+ return {
183
+ ...parseBuildDataObject(build, 'build'),
184
+ schema: parseLiteralString(build.schema, 'build.schema', 'build-scope/v1'),
185
+ ...(build.intent === undefined ? {} : { intent: parseBuildIntent(build.intent, 'build.intent') }),
186
+ ...(build.completeness === undefined
187
+ ? {}
188
+ : { completeness: parseBuildCompleteness(build.completeness, 'build.completeness') }),
189
+ ...(build.selectedBoardId === undefined
190
+ ? {}
191
+ : { selectedBoardId: expectString(build.selectedBoardId, 'build.selectedBoardId') }),
192
+ ...(build.selectedOffBoardWiringHarnessIds === undefined
193
+ ? {}
194
+ : {
195
+ selectedOffBoardWiringHarnessIds: parseOptionalStringArray(
196
+ build.selectedOffBoardWiringHarnessIds,
197
+ 'build.selectedOffBoardWiringHarnessIds',
198
+ ) ?? [],
199
+ }),
200
+ ...(build.alternateBoardIds === undefined
201
+ ? {}
202
+ : { alternateBoardIds: parseOptionalStringArray(build.alternateBoardIds, 'build.alternateBoardIds') ?? [] }),
203
+ ...(build.bomScope === undefined ? {} : { bomScope: expectString(build.bomScope, 'build.bomScope') }),
204
+ };
205
+ }
206
+
207
+ function parseBuildIntent(value: YamlValue | undefined, path: string): BuildIntent {
208
+ const intent = expectString(value, path);
209
+ if (intent === 'diy-build-artifact' || intent === 'schema-review-sample') {
210
+ return intent;
211
+ }
212
+ throw new Error(`${path}: expected diy-build-artifact or schema-review-sample`);
213
+ }
214
+
215
+ function parseBuildCompleteness(value: YamlValue | undefined, path: string): BuildCompleteness {
216
+ const completeness = expectString(value, path);
217
+ if (completeness === 'complete-selected-build' || completeness === 'partial-offboard-wiring') {
218
+ return completeness;
219
+ }
220
+ throw new Error(`${path}: expected complete-selected-build or partial-offboard-wiring`);
221
+ }
222
+
223
+ function parseBom(value: YamlValue | undefined): BuildBom | undefined {
224
+ if (value === undefined) {
225
+ return undefined;
226
+ }
227
+ const bom = expectObject(value, 'bom');
228
+ return {
229
+ ...parseBuildDataObject(bom, 'bom'),
230
+ schema: parseLiteralString(bom.schema, 'bom.schema', 'build-bom/v1'),
231
+ items: optionalArray(bom.items, 'bom.items').map(parseBomItem),
232
+ };
233
+ }
234
+
235
+ function parseBomItem(value: YamlValue, index: number): BuildBomItem {
236
+ const path = `bom.items[${index}]`;
237
+ const item = expectObject(value, path);
238
+ return {
239
+ ...parseBuildDataObject(item, path),
240
+ id: expectString(item.id, `${path}.id`),
241
+ refs: optionalArray(item.refs, `${path}.refs`).map((ref, refIndex) =>
242
+ parseBomRef(ref, `${path}.refs[${refIndex}]`)
243
+ ),
244
+ quantity: expectNumber(item.quantity, `${path}.quantity`),
245
+ ...(item.value === undefined ? {} : { value: expectString(item.value, `${path}.value`) }),
246
+ ...(item.partProfileId === undefined
247
+ ? {}
248
+ : { partProfileId: expectString(item.partProfileId, `${path}.partProfileId`) }),
249
+ ...(item.category === undefined ? {} : { category: expectString(item.category, `${path}.category`) }),
250
+ ...(item.sku === undefined ? {} : { sku: expectString(item.sku, `${path}.sku`) }),
251
+ };
252
+ }
253
+
254
+ function parseBomRef(value: YamlValue, path: string): BuildBomRef {
255
+ const ref = expectObject(value, path);
256
+ const kind = expectString(ref.kind, `${path}.kind`);
257
+ switch (kind) {
258
+ case 'component':
259
+ case 'device-interface-control':
260
+ case 'panel-element':
261
+ case 'board':
262
+ case 'freeform-build-item':
263
+ return {
264
+ ...parseBuildDataObject(ref, path),
265
+ kind,
266
+ ...(ref.componentId === undefined ? {} : { componentId: expectString(ref.componentId, `${path}.componentId`) }),
267
+ ...(ref.controlId === undefined ? {} : { controlId: expectString(ref.controlId, `${path}.controlId`) }),
268
+ ...(ref.panelElementId === undefined
269
+ ? {}
270
+ : { panelElementId: expectString(ref.panelElementId, `${path}.panelElementId`) }),
271
+ ...(ref.boardId === undefined ? {} : { boardId: expectString(ref.boardId, `${path}.boardId`) }),
272
+ ...(ref.label === undefined ? {} : { label: expectString(ref.label, `${path}.label`) }),
273
+ };
274
+ default:
275
+ throw new Error(`${path}.kind: expected component, device-interface-control, panel-element, board, or freeform-build-item`);
276
+ }
277
+ }
278
+
279
+ function parsePartProfiles(value: YamlValue | undefined): BuildPartProfileCatalog | undefined {
280
+ if (value === undefined) {
281
+ return undefined;
282
+ }
283
+ const catalog = expectObject(value, 'partProfiles');
284
+ return {
285
+ ...parseBuildDataObject(catalog, 'partProfiles'),
286
+ schema: parseLiteralString(catalog.schema, 'partProfiles.schema', 'part-profile-catalog/v1'),
287
+ ...(catalog.resolution === undefined
288
+ ? {}
289
+ : { resolution: expectString(catalog.resolution, 'partProfiles.resolution') }),
290
+ ...(catalog.units === undefined ? {} : { units: expectString(catalog.units, 'partProfiles.units') }),
291
+ profiles: optionalArray(catalog.profiles, 'partProfiles.profiles').map((profile, index) =>
292
+ parsePartProfile(profile, index)
293
+ ),
294
+ };
295
+ }
296
+
297
+ function parsePartProfile(value: YamlValue, index: number): BuildPartProfile {
298
+ const path = `partProfiles.profiles[${index}]`;
299
+ const profile = expectObject(value, path);
300
+ return {
301
+ ...parseBuildDataObject(profile, path),
302
+ id: expectString(profile.id, `${path}.id`),
303
+ ...(profile.kind === undefined ? {} : { kind: expectString(profile.kind, `${path}.kind`) }),
304
+ };
305
+ }
306
+
307
+ function parseFootprints(value: YamlValue | undefined): BoardFootprintCatalog | undefined {
308
+ if (value === undefined) {
309
+ return undefined;
310
+ }
311
+ const catalog = expectObject(value, 'footprints');
312
+ return {
313
+ ...parseBuildDataObject(catalog, 'footprints'),
314
+ schema: parseLiteralString(catalog.schema, 'footprints.schema', 'board-footprint-catalog/v1'),
315
+ ...(catalog.resolution === undefined ? {} : { resolution: expectString(catalog.resolution, 'footprints.resolution') }),
316
+ ...(catalog.units === undefined ? {} : { units: expectString(catalog.units, 'footprints.units') }),
317
+ footprints: optionalArray(catalog.footprints, 'footprints.footprints').map((footprint, index) =>
318
+ parseFootprint(footprint, index)
319
+ ),
320
+ };
321
+ }
322
+
323
+ function parseFootprint(value: YamlValue, index: number): BoardFootprint {
324
+ const path = `footprints.footprints[${index}]`;
325
+ const footprint = expectObject(value, path);
326
+ return {
327
+ ...parseBuildDataObject(footprint, path),
328
+ id: expectString(footprint.id, `${path}.id`),
329
+ ...(footprint.boardApplicability === undefined
330
+ ? {}
331
+ : { boardApplicability: parseBoardApplicability(footprint.boardApplicability, `${path}.boardApplicability`) }),
332
+ };
333
+ }
334
+
335
+ function parseBoardApplicability(value: YamlValue, path: string): BoardApplicability {
336
+ const applicability = expectObject(value, path);
337
+ return {
338
+ ...parseBuildDataObject(applicability, path),
339
+ family: parseBoardFamily(applicability.family, `${path}.family`),
340
+ kind: parseBoardKind(applicability.kind, `${path}.kind`),
341
+ ...(applicability.subtype === undefined
342
+ ? {}
343
+ : { subtype: parseBoardSubtype(applicability.subtype, `${path}.subtype`) }),
344
+ };
345
+ }
346
+
347
+ function parseOffBoardWiring(value: YamlValue | undefined): OffBoardWiringPlan | undefined {
348
+ if (value === undefined) {
349
+ return undefined;
350
+ }
351
+ const plan = expectObject(value, 'offBoardWiring');
352
+ return {
353
+ ...parseBuildDataObject(plan, 'offBoardWiring'),
354
+ schema: parseLiteralString(plan.schema, 'offBoardWiring.schema', 'offboard-wiring/v1'),
355
+ ...(plan.source === undefined ? {} : { source: expectString(plan.source, 'offBoardWiring.source') }),
356
+ ...(plan.coverage === undefined
357
+ ? {}
358
+ : { coverage: parseOffBoardWiringCoverage(plan.coverage, 'offBoardWiring.coverage') }),
359
+ harnesses: optionalArray(plan.harnesses, 'offBoardWiring.harnesses').map(parseOffBoardWiringHarness),
360
+ };
361
+ }
362
+
363
+ function parseOffBoardWiringCoverage(value: YamlValue | undefined, path: string): OffBoardWiringCoverage {
364
+ const coverage = expectString(value, path);
365
+ if (coverage === 'selected-build-complete' || coverage === 'representative-selected-build-endpoints') {
366
+ return coverage;
367
+ }
368
+ throw new Error(`${path}: expected selected-build-complete or representative-selected-build-endpoints`);
369
+ }
370
+
371
+ function parseOffBoardWiringHarness(value: YamlValue, index: number): OffBoardWiringHarness {
372
+ const path = `offBoardWiring.harnesses[${index}]`;
373
+ const harness = expectObject(value, path);
374
+ return {
375
+ ...parseBuildDataObject(harness, path),
376
+ id: expectString(harness.id, `${path}.id`),
377
+ ...(harness.status === undefined
378
+ ? {}
379
+ : { status: parseOffBoardWiringHarnessStatus(harness.status, `${path}.status`) }),
380
+ ...(harness.notes === undefined ? {} : { notes: expectString(harness.notes, `${path}.notes`) }),
381
+ endpoints: optionalArray(harness.endpoints, `${path}.endpoints`).map((endpoint, endpointIndex) =>
382
+ parseOffBoardWiringEndpoint(endpoint, `${path}.endpoints[${endpointIndex}]`)
383
+ ),
384
+ connections: optionalArray(harness.connections, `${path}.connections`).map((connection, connectionIndex) =>
385
+ parseOffBoardWiringConnection(connection, `${path}.connections[${connectionIndex}]`)
386
+ ),
387
+ };
388
+ }
389
+
390
+ function parseOffBoardWiringHarnessStatus(value: YamlValue | undefined, path: string): OffBoardWiringHarnessStatus {
391
+ const status = expectString(value, path);
392
+ if (status === 'complete' || status === 'partial' || status === 'candidate') {
393
+ return status;
394
+ }
395
+ throw new Error(`${path}: expected complete, partial, or candidate`);
396
+ }
397
+
398
+ function parseOffBoardWiringEndpoint(value: YamlValue, path: string): OffBoardWiringEndpoint {
399
+ const endpoint = expectObject(value, path);
400
+ const kind = expectString(endpoint.kind, `${path}.kind`);
401
+ switch (kind) {
402
+ case 'panel-component-terminal':
403
+ case 'board-terminal':
404
+ case 'power-terminal':
405
+ case 'footswitch-terminal':
406
+ case 'free-wire-label':
407
+ return {
408
+ ...parseBuildDataObject(endpoint, path),
409
+ id: expectString(endpoint.id, `${path}.id`),
410
+ kind,
411
+ ...(endpoint.componentId === undefined
412
+ ? {}
413
+ : { componentId: expectString(endpoint.componentId, `${path}.componentId`) }),
414
+ ...(endpoint.terminalName === undefined
415
+ ? {}
416
+ : { terminalName: expectString(endpoint.terminalName, `${path}.terminalName`) }),
417
+ ...(endpoint.panelElementId === undefined
418
+ ? {}
419
+ : { panelElementId: expectString(endpoint.panelElementId, `${path}.panelElementId`) }),
420
+ ...(endpoint.boardId === undefined ? {} : { boardId: expectString(endpoint.boardId, `${path}.boardId`) }),
421
+ ...(endpoint.terminalId === undefined
422
+ ? {}
423
+ : { terminalId: expectString(endpoint.terminalId, `${path}.terminalId`) }),
424
+ ...(endpoint.label === undefined ? {} : { label: expectString(endpoint.label, `${path}.label`) }),
425
+ };
426
+ default:
427
+ throw new Error(`${path}.kind: expected a supported off-board wiring endpoint kind`);
428
+ }
429
+ }
430
+
431
+ function parseOffBoardWiringConnection(value: YamlValue, path: string): OffBoardWiringConnection {
432
+ const connection = expectObject(value, path);
433
+ return {
434
+ ...parseBuildDataObject(connection, path),
435
+ id: expectString(connection.id, `${path}.id`),
436
+ fromEndpointId: expectString(connection.fromEndpointId, `${path}.fromEndpointId`),
437
+ toEndpointId: expectString(connection.toEndpointId, `${path}.toEndpointId`),
438
+ ...(connection.signalRef === undefined
439
+ ? {}
440
+ : { signalRef: parseBuildDataObject(connection.signalRef, `${path}.signalRef`) }),
441
+ ...(connection.wire === undefined ? {} : { wire: parseBuildDataObject(connection.wire, `${path}.wire`) }),
442
+ };
443
+ }
444
+
445
+ function parseBoards(value: YamlValue | undefined): readonly BoardRealization[] | undefined {
446
+ if (value === undefined) {
447
+ return undefined;
448
+ }
449
+ return optionalArray(value, 'boards').map(parseBoard);
450
+ }
451
+
452
+ function parseBoard(value: YamlValue, index: number): BoardRealization {
453
+ const path = `boards[${index}]`;
454
+ const board = expectObject(value, path);
455
+ const sourceCircuit = board.sourceCircuit === undefined
456
+ ? undefined
457
+ : parseBoardSourceCircuit(board.sourceCircuit, `${path}.sourceCircuit`);
458
+ return {
459
+ ...parseBuildDataObject(board, path),
460
+ id: expectString(board.id, `${path}.id`),
461
+ schema: parseLiteralString(board.schema, `${path}.schema`, 'circuit-board/v1'),
462
+ family: parseBoardFamily(board.family, `${path}.family`),
463
+ kind: parseBoardKind(board.kind, `${path}.kind`),
464
+ ...(board.subtype === undefined ? {} : { subtype: parseBoardSubtype(board.subtype, `${path}.subtype`) }),
465
+ ...(board.source === undefined ? {} : { source: expectString(board.source, `${path}.source`) }),
466
+ ...(board.units === undefined ? {} : { units: expectString(board.units, `${path}.units`) }),
467
+ ...(board.locked === undefined ? {} : { locked: expectBoolean(board.locked, `${path}.locked`) }),
468
+ ...(sourceCircuit === undefined ? {} : { sourceCircuit }),
469
+ edgeTerminals: optionalArray(board.edgeTerminals, `${path}.edgeTerminals`).map((terminal, terminalIndex) =>
470
+ parseBoardEdgeTerminal(terminal, `${path}.edgeTerminals[${terminalIndex}]`)
471
+ ),
472
+ footprintPlacements: optionalArray(board.footprintPlacements, `${path}.footprintPlacements`).map((placement, placementIndex) =>
473
+ parseBoardFootprintPlacement(placement, `${path}.footprintPlacements[${placementIndex}]`)
474
+ ),
475
+ ...(board.netlist === undefined ? {} : { netlist: parseBoardNetlist(board.netlist, `${path}.netlist`) }),
476
+ routes: optionalArray(board.routes, `${path}.routes`).map((route, routeIndex) =>
477
+ parseBoardRoute(route, `${path}.routes[${routeIndex}]`)
478
+ ),
479
+ ...(board.zones === undefined ? {} : { zones: parseBuildDataObjectArray(board.zones, `${path}.zones`) }),
480
+ ...(board.drills === undefined ? {} : { drills: parseBuildDataObjectArray(board.drills, `${path}.drills`) }),
481
+ ...(board.review === undefined ? {} : { review: parseBuildDataObject(board.review, `${path}.review`) }),
482
+ };
483
+ }
484
+
485
+ function parseBoardSourceCircuit(value: YamlValue, path: string): BoardSourceCircuitHash {
486
+ const sourceCircuit = expectObject(value, path);
487
+ const hash = expectString(sourceCircuit.hash, `${path}.hash`);
488
+ if (!/^sha256:[0-9a-f]{64}$/.test(hash)) {
489
+ throw new Error(`${path}.hash: expected sha256:<64 lowercase hex characters>`);
490
+ }
491
+ return {
492
+ ...parseBuildDataObject(sourceCircuit, path),
493
+ schema: parseLiteralString(sourceCircuit.schema, `${path}.schema`, 'canonical-circuit-facts-hash/v1'),
494
+ hashAlgorithm: parseLiteralString(sourceCircuit.hashAlgorithm, `${path}.hashAlgorithm`, 'sha256'),
495
+ hash,
496
+ };
497
+ }
498
+
499
+ function parseBoardEdgeTerminal(value: YamlValue, path: string): BoardEdgeTerminal {
500
+ const terminal = expectObject(value, path);
501
+ return {
502
+ ...parseBuildDataObject(terminal, path),
503
+ id: expectString(terminal.id, `${path}.id`),
504
+ ...(terminal.role === undefined ? {} : { role: expectString(terminal.role, `${path}.role`) }),
505
+ ...(terminal.terminalRef === undefined
506
+ ? {}
507
+ : { terminalRef: parseComponentTerminalRef(terminal.terminalRef, `${path}.terminalRef`) }),
508
+ ...(terminal.hole === undefined ? {} : { hole: parseBoardHole(terminal.hole, `${path}.hole`) }),
509
+ };
510
+ }
511
+
512
+ function parseBoardFootprintPlacement(value: YamlValue, path: string): BoardFootprintPlacement {
513
+ const placement = expectObject(value, path);
514
+ return {
515
+ ...parseBuildDataObject(placement, path),
516
+ componentId: expectString(placement.componentId, `${path}.componentId`),
517
+ footprintId: expectString(placement.footprintId, `${path}.footprintId`),
518
+ ...(placement.atGrid === undefined ? {} : { atGrid: parseBoardHole(placement.atGrid, `${path}.atGrid`) }),
519
+ ...(placement.atMm === undefined ? {} : { atMm: parsePoint(placement.atMm, `${path}.atMm`) }),
520
+ ...(placement.rotationDeg === undefined ? {} : { rotationDeg: expectNumber(placement.rotationDeg, `${path}.rotationDeg`) }),
521
+ pads: optionalArray(placement.pads, `${path}.pads`).map((pad, padIndex) =>
522
+ parseBoardPlacedPad(pad, `${path}.pads[${padIndex}]`)
523
+ ),
524
+ };
525
+ }
526
+
527
+ function parseBoardPlacedPad(value: YamlValue, path: string): BoardPlacedPad {
528
+ const pad = expectObject(value, path);
529
+ return {
530
+ ...parseBuildDataObject(pad, path),
531
+ padId: expectString(pad.padId, `${path}.padId`),
532
+ ...(pad.terminalName === undefined ? {} : { terminalName: expectString(pad.terminalName, `${path}.terminalName`) }),
533
+ ...(pad.hole === undefined ? {} : { hole: parseBoardHole(pad.hole, `${path}.hole`) }),
534
+ ...(pad.positionMm === undefined ? {} : { positionMm: parsePoint(pad.positionMm, `${path}.positionMm`) }),
535
+ };
536
+ }
537
+
538
+ function parseBoardNetlist(value: YamlValue, path: string): BoardNetlist {
539
+ const netlist = expectObject(value, path);
540
+ return {
541
+ ...parseBuildDataObject(netlist, path),
542
+ ...(netlist.source === undefined ? {} : { source: expectString(netlist.source, `${path}.source`) }),
543
+ nets: optionalArray(netlist.nets, `${path}.nets`).map((net, netIndex) =>
544
+ parseBoardNet(net, `${path}.nets[${netIndex}]`)
545
+ ),
546
+ };
547
+ }
548
+
549
+ function parseBoardNet(value: YamlValue, path: string): BoardNet {
550
+ const net = expectObject(value, path);
551
+ return {
552
+ ...parseBuildDataObject(net, path),
553
+ id: expectString(net.id, `${path}.id`),
554
+ ...(net.name === undefined ? {} : { name: expectString(net.name, `${path}.name`) }),
555
+ members: optionalArray(net.members, `${path}.members`).map((member, memberIndex) =>
556
+ parseBoardNetMember(member, `${path}.members[${memberIndex}]`)
557
+ ),
558
+ };
559
+ }
560
+
561
+ function parseBoardNetMember(value: YamlValue, path: string): BoardNetMember {
562
+ const member = expectObject(value, path);
563
+ return {
564
+ ...parseBuildDataObject(member, path),
565
+ componentId: expectString(member.componentId, `${path}.componentId`),
566
+ terminalName: expectString(member.terminalName, `${path}.terminalName`),
567
+ ...(member.padId === undefined ? {} : { padId: expectString(member.padId, `${path}.padId`) }),
568
+ ...(member.terminalId === undefined ? {} : { terminalId: expectString(member.terminalId, `${path}.terminalId`) }),
569
+ };
570
+ }
571
+
572
+ function parseBoardRoute(value: YamlValue, path: string): BoardRoute {
573
+ const route = expectObject(value, path);
574
+ return {
575
+ ...parseBuildDataObject(route, path),
576
+ id: expectString(route.id, `${path}.id`),
577
+ ...(route.netRef === undefined ? {} : { netRef: parseBuildDataObject(route.netRef, `${path}.netRef`) }),
578
+ ...(route.locked === undefined ? {} : { locked: expectBoolean(route.locked, `${path}.locked`) }),
579
+ ...(route.conductors === undefined
580
+ ? {}
581
+ : { conductors: parseBuildDataObjectArray(route.conductors, `${path}.conductors`) }),
582
+ ...(route.copper === undefined ? {} : { copper: parseBuildDataObjectArray(route.copper, `${path}.copper`) }),
583
+ ...(route.vias === undefined ? {} : { vias: parseBuildDataObjectArray(route.vias, `${path}.vias`) }),
584
+ ...(route.zones === undefined ? {} : { zones: parseBuildDataObjectArray(route.zones, `${path}.zones`) }),
585
+ ...(route.drills === undefined ? {} : { drills: parseBuildDataObjectArray(route.drills, `${path}.drills`) }),
586
+ };
587
+ }
588
+
589
+ function parseComponentTerminalRef(value: YamlValue, path: string): ComponentTerminalRef {
590
+ const ref = expectObject(value, path);
591
+ return {
592
+ ...parseBuildDataObject(ref, path),
593
+ componentId: expectString(ref.componentId, `${path}.componentId`),
594
+ terminalName: expectString(ref.terminalName, `${path}.terminalName`),
595
+ };
596
+ }
597
+
598
+ function parseBoardHole(value: YamlValue, path: string) {
599
+ const hole = expectObject(value, path);
600
+ return {
601
+ ...parseBuildDataObject(hole, path),
602
+ row: expectPositiveInteger(hole.row, `${path}.row`),
603
+ column: expectPositiveInteger(hole.column, `${path}.column`),
604
+ };
605
+ }
606
+
607
+ function parseBoardFamily(value: YamlValue | undefined, path: string): BoardFamily {
608
+ const family = expectString(value, path);
609
+ if (family === 'prototype-board' || family === 'fabricated-board') {
610
+ return family;
611
+ }
612
+ throw new Error(`${path}: expected prototype-board or fabricated-board`);
613
+ }
614
+
615
+ function parseBoardKind(value: YamlValue | undefined, path: string): BoardKind {
616
+ const kind = expectString(value, path);
617
+ switch (kind) {
618
+ case 'stripboard':
619
+ case 'perfboard':
620
+ case 'breadboard-pattern':
621
+ case 'pcb':
622
+ return kind;
623
+ default:
624
+ throw new Error(`${path}: expected stripboard, perfboard, breadboard-pattern, or pcb`);
625
+ }
626
+ }
627
+
628
+ function parseBoardSubtype(value: YamlValue | undefined, path: string): BoardSubtype {
629
+ const subtype = expectString(value, path);
630
+ switch (subtype) {
631
+ case 'veroboard':
632
+ case 'isolated-pad':
633
+ case 'solderable-half-breadboard':
634
+ case 'single-sided-through-hole':
635
+ case 'two-layer-through-hole':
636
+ return subtype;
637
+ default:
638
+ throw new Error(`${path}: expected a supported board subtype`);
639
+ }
640
+ }
641
+
642
+ function parseLiteralString<T extends string>(value: YamlValue | undefined, path: string, expected: T): T {
643
+ const actual = expectString(value, path);
644
+ if (actual === expected) {
645
+ return expected;
646
+ }
647
+ throw new Error(`${path}: expected ${expected}`);
648
+ }
649
+
650
+ function parseBuildDataObjectArray(value: YamlValue | undefined, path: string): readonly VdspBuildDataObject[] {
651
+ return optionalArray(value, path).map((item, index) => parseBuildDataObject(item, `${path}[${index}]`));
652
+ }
653
+
654
+ function parseBuildDataObject(value: YamlValue | undefined, path: string): VdspBuildDataObject {
655
+ const object = expectObject(value, path);
656
+ const out: Record<string, VdspBuildDataValue | undefined> = {};
657
+ for (const [key, child] of Object.entries(object)) {
658
+ out[key] = parseBuildDataValue(child, `${path}.${key}`);
659
+ }
660
+ return out;
661
+ }
662
+
663
+ function parseBuildDataValue(value: YamlValue, path: string): VdspBuildDataValue {
664
+ if (isScalar(value)) {
665
+ return value;
666
+ }
667
+ if (Array.isArray(value)) {
668
+ return value.map((item, index) => parseBuildDataValue(item, `${path}[${index}]`));
669
+ }
670
+ if (isYamlObject(value)) {
671
+ return parseBuildDataObject(value, path);
672
+ }
673
+ throw new Error(`${path}: expected v3 build data value`);
674
+ }
675
+
94
676
  function parseControlGroups(value: YamlValue | undefined): readonly ControlGroup[] | undefined {
95
677
  if (value === undefined) {
96
678
  return undefined;
@@ -678,7 +1260,7 @@ function parseSource(value: YamlValue | undefined): DocumentSource {
678
1260
  return parseStringRecord(value, 'source');
679
1261
  }
680
1262
 
681
- function parsePanel(value: YamlValue | undefined): PanelPlacementMetadata | undefined {
1263
+ function parsePanel(value: YamlValue | undefined, allowV3PhysicalFields: boolean): PanelPlacementMetadata | undefined {
682
1264
  if (value === undefined) {
683
1265
  return undefined;
684
1266
  }
@@ -686,7 +1268,9 @@ function parsePanel(value: YamlValue | undefined): PanelPlacementMetadata | unde
686
1268
 
687
1269
  if (panel.faces !== undefined) {
688
1270
  return {
689
- faces: optionalArray(panel.faces, 'panel.faces').map((item, index) => parsePanelFace(item, index)),
1271
+ faces: optionalArray(panel.faces, 'panel.faces').map((item, index) =>
1272
+ parsePanelFace(item, index, allowV3PhysicalFields)
1273
+ ),
690
1274
  };
691
1275
  }
692
1276
 
@@ -701,21 +1285,30 @@ function parsePanel(value: YamlValue | undefined): PanelPlacementMetadata | unde
701
1285
  faces: [{
702
1286
  id: 'top',
703
1287
  layout,
704
- elements: parsePanelElements(elementsValue, layout, elementsPath),
1288
+ elements: parsePanelElements(elementsValue, layout, elementsPath, allowV3PhysicalFields),
705
1289
  }],
706
1290
  };
707
1291
  }
708
1292
 
709
- function parsePanelFace(value: YamlValue, index: number): PanelPlacementMetadata['faces'][number] {
1293
+ function parsePanelFace(
1294
+ value: YamlValue,
1295
+ index: number,
1296
+ allowV3PhysicalFields: boolean,
1297
+ ): PanelPlacementMetadata['faces'][number] {
710
1298
  const path = `panel.faces[${index}]`;
711
1299
  const face = expectObject(value, path);
712
1300
  const label = parseOptionalString(face.label, `${path}.label`);
713
1301
  const layout = parsePanelLayout(face.layout, `${path}.layout`);
1302
+ if (!allowV3PhysicalFields && face.geometry !== undefined) {
1303
+ throw new Error(`${path}.geometry: requires schema ${INTERCHANGE_SCHEMA_V3}`);
1304
+ }
1305
+ const geometry = allowV3PhysicalFields ? parseOptionalPanelFaceGeometry(face.geometry, `${path}.geometry`) : undefined;
714
1306
  return {
715
1307
  id: expectString(face.id, `${path}.id`),
716
1308
  ...(label === undefined ? {} : { label }),
717
1309
  layout,
718
- elements: parsePanelElements(face.elements, layout, `${path}.elements`),
1310
+ ...(geometry === undefined ? {} : { geometry }),
1311
+ elements: parsePanelElements(face.elements, layout, `${path}.elements`, allowV3PhysicalFields),
719
1312
  };
720
1313
  }
721
1314
 
@@ -737,13 +1330,25 @@ function parsePanelElements(
737
1330
  value: YamlValue | undefined,
738
1331
  layout: PanelGridLayout,
739
1332
  path: string,
1333
+ allowV3PhysicalFields: boolean,
740
1334
  ): PanelPlacementMetadata['faces'][number]['elements'] {
741
1335
  return optionalArray(value, path).map((item, index) => {
742
1336
  const elementPath = `${path}[${index}]`;
743
1337
  const element = expectObject(item, elementPath);
744
1338
  const label = parseOptionalString(element.label, `${elementPath}.label`);
745
1339
  const interfaceControlId = parseOptionalString(element.interfaceControlId, `${elementPath}.interfaceControlId`);
1340
+ if (!allowV3PhysicalFields && element.id !== undefined) {
1341
+ throw new Error(`${elementPath}.id: requires schema ${INTERCHANGE_SCHEMA_V3}`);
1342
+ }
1343
+ if (!allowV3PhysicalFields && element.physical !== undefined) {
1344
+ throw new Error(`${elementPath}.physical: requires schema ${INTERCHANGE_SCHEMA_V3}`);
1345
+ }
1346
+ const id = allowV3PhysicalFields ? parseOptionalString(element.id, `${elementPath}.id`) : undefined;
1347
+ const physical = allowV3PhysicalFields
1348
+ ? parseOptionalPanelElementPhysical(element.physical, `${elementPath}.physical`)
1349
+ : undefined;
746
1350
  return {
1351
+ ...(id === undefined ? {} : { id }),
747
1352
  bind: parsePanelElementBinding(element, elementPath),
748
1353
  kind: parsePanelControlKind(
749
1354
  element.kind ?? element.controlKind,
@@ -754,10 +1359,61 @@ function parsePanelElements(
754
1359
  grid: parsePanelGridPosition(element.grid, `${elementPath}.grid`, layout),
755
1360
  ...(label === undefined ? {} : { label }),
756
1361
  ...(interfaceControlId === undefined ? {} : { interfaceControlId }),
1362
+ ...(physical === undefined ? {} : { physical }),
757
1363
  };
758
1364
  });
759
1365
  }
760
1366
 
1367
+ function parseOptionalPanelFaceGeometry(
1368
+ value: YamlValue | undefined,
1369
+ path: string,
1370
+ ): PanelFaceGeometry | undefined {
1371
+ if (value === undefined) {
1372
+ return undefined;
1373
+ }
1374
+ const geometry = expectObject(value, path);
1375
+ return {
1376
+ ...parseBuildDataObject(geometry, path),
1377
+ ...(geometry.units === undefined ? {} : { units: expectString(geometry.units, `${path}.units`) }),
1378
+ ...(geometry.surface === undefined ? {} : { surface: expectString(geometry.surface, `${path}.surface`) }),
1379
+ ...(geometry.usableRectMm === undefined
1380
+ ? {}
1381
+ : { usableRectMm: parseMillimeterRect(geometry.usableRectMm, `${path}.usableRectMm`) }),
1382
+ };
1383
+ }
1384
+
1385
+ function parseOptionalPanelElementPhysical(
1386
+ value: YamlValue | undefined,
1387
+ path: string,
1388
+ ): PanelElementPhysicalPlacement | undefined {
1389
+ if (value === undefined) {
1390
+ return undefined;
1391
+ }
1392
+ const physical = expectObject(value, path);
1393
+ return {
1394
+ ...parseBuildDataObject(physical, path),
1395
+ ...(physical.units === undefined ? {} : { units: expectString(physical.units, `${path}.units`) }),
1396
+ ...(physical.centerMm === undefined ? {} : { centerMm: parsePoint(physical.centerMm, `${path}.centerMm`) }),
1397
+ ...(physical.drillDiameterMm === undefined
1398
+ ? {}
1399
+ : { drillDiameterMm: expectNumber(physical.drillDiameterMm, `${path}.drillDiameterMm`) }),
1400
+ ...(physical.partProfileId === undefined
1401
+ ? {}
1402
+ : { partProfileId: expectString(physical.partProfileId, `${path}.partProfileId`) }),
1403
+ ...(physical.locked === undefined ? {} : { locked: expectBoolean(physical.locked, `${path}.locked`) }),
1404
+ };
1405
+ }
1406
+
1407
+ function parseMillimeterRect(value: YamlValue | undefined, path: string) {
1408
+ const rect = expectObject(value, path);
1409
+ return {
1410
+ x: expectNumber(rect.x, `${path}.x`),
1411
+ y: expectNumber(rect.y, `${path}.y`),
1412
+ width: expectNumber(rect.width, `${path}.width`),
1413
+ height: expectNumber(rect.height, `${path}.height`),
1414
+ };
1415
+ }
1416
+
761
1417
  function parsePanelElementBinding(element: YamlObject, path: string): PanelElementBinding {
762
1418
  if (element.bind !== undefined) {
763
1419
  const bind = expectObject(element.bind, `${path}.bind`);
@@ -857,11 +1513,13 @@ function parsePanelControlKind(value: YamlValue | undefined, path: string): Pane
857
1513
  case 'knob':
858
1514
  case 'slider':
859
1515
  case 'switch':
1516
+ case 'selector':
1517
+ case 'footswitch':
860
1518
  case 'led':
861
1519
  case 'jack':
862
1520
  return kind;
863
1521
  default:
864
- throw new Error(`${path}: expected knob, slider, switch, led, or jack`);
1522
+ throw new Error(`${path}: expected knob, slider, switch, selector, footswitch, led, or jack`);
865
1523
  }
866
1524
  }
867
1525