@vessel-dsp/core 0.5.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.
- package/LICENSE.md +21 -0
- package/README.md +6 -0
- package/package.json +56 -0
- package/src/editor/commands.ts +344 -0
- package/src/editor/factory.ts +148 -0
- package/src/editor/history.ts +142 -0
- package/src/editor/index.ts +11 -0
- package/src/editor/layout.ts +207 -0
- package/src/formats/circuit-json/serializer.ts +1410 -0
- package/src/formats/document.ts +274 -0
- package/src/formats/interchange/parser.ts +1165 -0
- package/src/formats/interchange/serializer.ts +594 -0
- package/src/formats/ltspice/catalog.ts +181 -0
- package/src/formats/ltspice/encoding.ts +151 -0
- package/src/formats/ltspice/parser.ts +432 -0
- package/src/formats/ltspice/serializer.ts +169 -0
- package/src/formats/schx/catalog.ts +439 -0
- package/src/formats/schx/parser.ts +261 -0
- package/src/formats/schx/runtime-descriptors.ts +502 -0
- package/src/formats/schx/serializer.ts +211 -0
- package/src/formats/schx/transforms.ts +38 -0
- package/src/formats/spice/parser.ts +373 -0
- package/src/formats/spice/serializer.ts +43 -0
- package/src/index.ts +205 -0
- package/src/model/connectivity.ts +239 -0
- package/src/model/netlist.ts +375 -0
- package/src/model/properties.ts +101 -0
- package/src/model/quantity.ts +173 -0
- package/src/model/types.ts +309 -0
- package/src/model/validation.ts +985 -0
- package/src/model/wires.ts +86 -0
- package/src/panel/extract.ts +878 -0
- package/src/panel/index.ts +39 -0
- package/src/panel/knobs.ts +70 -0
- package/src/panel/protocol.ts +117 -0
- package/src/panel/types.ts +180 -0
- package/src/preview/bounds.ts +85 -0
- package/src/preview/box-layout.ts +24 -0
- package/src/preview/colors.ts +43 -0
- package/src/preview/hanging.ts +94 -0
- package/src/preview/junctions.ts +94 -0
- package/src/preview/label-layout.ts +90 -0
- package/src/preview/ports.ts +101 -0
- package/src/preview/renderable-wires.ts +113 -0
- package/src/preview/routing.ts +15 -0
- package/src/preview/snap.ts +104 -0
- package/src/preview/symbols/analog-switch.svg +17 -0
- package/src/preview/symbols/battery.svg +16 -0
- package/src/preview/symbols/bbd.svg +21 -0
- package/src/preview/symbols/bjt-npn.svg +16 -0
- package/src/preview/symbols/bjt-pnp.svg +17 -0
- package/src/preview/symbols/capacitor-electrolytic.svg +13 -0
- package/src/preview/symbols/capacitor.svg +12 -0
- package/src/preview/symbols/current-source.svg +14 -0
- package/src/preview/symbols/delay-ic.svg +22 -0
- package/src/preview/symbols/diode-schottky.svg +12 -0
- package/src/preview/symbols/diode-zener.svg +12 -0
- package/src/preview/symbols/diode.svg +13 -0
- package/src/preview/symbols/flipflop.svg +20 -0
- package/src/preview/symbols/ground.svg +12 -0
- package/src/preview/symbols/ic-block.svg +20 -0
- package/src/preview/symbols/ic.svg +19 -0
- package/src/preview/symbols/inductor.svg +11 -0
- package/src/preview/symbols/jack-input.svg +16 -0
- package/src/preview/symbols/jack-output.svg +16 -0
- package/src/preview/symbols/jfet-junction-n.svg +17 -0
- package/src/preview/symbols/jfet-n.svg +17 -0
- package/src/preview/symbols/jfet-p.svg +17 -0
- package/src/preview/symbols/label.svg +8 -0
- package/src/preview/symbols/led.svg +18 -0
- package/src/preview/symbols/mosfet-n.svg +21 -0
- package/src/preview/symbols/mosfet-p.svg +21 -0
- package/src/preview/symbols/named-wire.svg +11 -0
- package/src/preview/symbols/opamp.svg +21 -0
- package/src/preview/symbols/optocoupler.svg +30 -0
- package/src/preview/symbols/ota.svg +20 -0
- package/src/preview/symbols/pentode.svg +25 -0
- package/src/preview/symbols/photoresistor.svg +19 -0
- package/src/preview/symbols/port.svg +8 -0
- package/src/preview/symbols/potentiometer.svg +15 -0
- package/src/preview/symbols/power-amp.svg +20 -0
- package/src/preview/symbols/rail.svg +11 -0
- package/src/preview/symbols/regulator.svg +13 -0
- package/src/preview/symbols/relay.svg +20 -0
- package/src/preview/symbols/resistor.svg +11 -0
- package/src/preview/symbols/svg-content.ts +59 -0
- package/src/preview/symbols/switch-3pdt.svg +32 -0
- package/src/preview/symbols/switch-rotary.svg +23 -0
- package/src/preview/symbols/switch-spdt.svg +16 -0
- package/src/preview/symbols/switch-spst.svg +14 -0
- package/src/preview/symbols/switch-toggle.svg +14 -0
- package/src/preview/symbols/transformer.svg +17 -0
- package/src/preview/symbols/triode.svg +17 -0
- package/src/preview/symbols/tube-diode.svg +13 -0
- package/src/preview/symbols/unsupported.svg +8 -0
- package/src/preview/symbols/variable-resistor.svg +13 -0
- package/src/preview/symbols/voltage-source.svg +15 -0
- package/src/preview/symbols.ts +207 -0
- package/src/preview/wire-chains.ts +153 -0
|
@@ -0,0 +1,1165 @@
|
|
|
1
|
+
import { isParsedQuantity } from '../../model/properties';
|
|
2
|
+
import type {
|
|
3
|
+
CircuitDocument,
|
|
4
|
+
CircuitDocumentDevice,
|
|
5
|
+
CircuitDocumentDeviceKind,
|
|
6
|
+
Component,
|
|
7
|
+
ComponentKind,
|
|
8
|
+
ControlApplicabilityPredicate,
|
|
9
|
+
ControlContext,
|
|
10
|
+
ControlGroup,
|
|
11
|
+
DeviceInterface,
|
|
12
|
+
DeviceInterfaceBinding,
|
|
13
|
+
DeviceInterfaceControlKind,
|
|
14
|
+
ControlInterface,
|
|
15
|
+
ControlInterfaceAssignmentHint,
|
|
16
|
+
ControlInterfaceConnector,
|
|
17
|
+
ControlInterfacePolarity,
|
|
18
|
+
ControlInterfaceRole,
|
|
19
|
+
ControlOutput,
|
|
20
|
+
ControlOutputSwitchMode,
|
|
21
|
+
DocumentSource,
|
|
22
|
+
PanelColumnOrder,
|
|
23
|
+
PanelControlKind,
|
|
24
|
+
PanelElementBinding,
|
|
25
|
+
PanelGridLayout,
|
|
26
|
+
PanelGridIndexing,
|
|
27
|
+
PanelGridPosition,
|
|
28
|
+
PanelPlacementMetadata,
|
|
29
|
+
PanelRowOrder,
|
|
30
|
+
ParsedQuantity,
|
|
31
|
+
Point,
|
|
32
|
+
PropertyValue,
|
|
33
|
+
Rotation,
|
|
34
|
+
Terminal,
|
|
35
|
+
Warning,
|
|
36
|
+
Wire,
|
|
37
|
+
} from '../../model/types';
|
|
38
|
+
|
|
39
|
+
type YamlScalar = string | number | boolean | null;
|
|
40
|
+
type YamlValue = YamlScalar | readonly YamlValue[] | YamlObject;
|
|
41
|
+
type YamlObject = { [key: string]: YamlValue };
|
|
42
|
+
|
|
43
|
+
type YamlLine = Readonly<{
|
|
44
|
+
indent: number;
|
|
45
|
+
text: string;
|
|
46
|
+
lineNumber: number;
|
|
47
|
+
}>;
|
|
48
|
+
|
|
49
|
+
type Cursor = {
|
|
50
|
+
index: number;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
type ParsedPair = Readonly<{
|
|
54
|
+
key: string;
|
|
55
|
+
rest: string;
|
|
56
|
+
}>;
|
|
57
|
+
|
|
58
|
+
const INTERCHANGE_SCHEMA = 'circuit-interchange/v2';
|
|
59
|
+
|
|
60
|
+
export function parseInterchangeYaml(source: string): CircuitDocument {
|
|
61
|
+
const value = parseYamlSubset(source);
|
|
62
|
+
const root = expectObject(value, 'root');
|
|
63
|
+
const schema = expectString(root.schema, 'schema');
|
|
64
|
+
if (schema !== INTERCHANGE_SCHEMA) {
|
|
65
|
+
throw new Error(`unsupported interchange schema: ${schema}`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const panel = parsePanel(root.panel);
|
|
69
|
+
const controlInterfaces = parseControlInterfaces(root.controlInterfaces);
|
|
70
|
+
const device = parseDevice(root.device);
|
|
71
|
+
const controlOutputs = parseControlOutputs(root.controlOutputs);
|
|
72
|
+
const controlGroups = parseControlGroups(root.controlGroups);
|
|
73
|
+
const controlContexts = parseControlContexts(root.controlContexts);
|
|
74
|
+
const deviceInterface = parseDeviceInterface(root.deviceInterface);
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
metadata: parseMetadata(root.metadata),
|
|
78
|
+
source: parseSource(root.source),
|
|
79
|
+
...(device === undefined ? {} : { device }),
|
|
80
|
+
...(controlGroups === undefined ? {} : { controlGroups }),
|
|
81
|
+
...(controlContexts === undefined ? {} : { controlContexts }),
|
|
82
|
+
...(deviceInterface === undefined ? {} : { deviceInterface }),
|
|
83
|
+
...(panel === undefined ? {} : { panel }),
|
|
84
|
+
...(controlInterfaces === undefined ? {} : { controlInterfaces }),
|
|
85
|
+
...(controlOutputs === undefined ? {} : { controlOutputs }),
|
|
86
|
+
components: parseComponents(root.components),
|
|
87
|
+
wires: parseWires(root.wires),
|
|
88
|
+
directives: parseStringArray(root.directives, 'directives'),
|
|
89
|
+
warnings: parseWarnings(root.diagnostics),
|
|
90
|
+
rawAttributes: parseStringRecord(root.rawAttributes, 'rawAttributes'),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function parseControlGroups(value: YamlValue | undefined): readonly ControlGroup[] | undefined {
|
|
95
|
+
if (value === undefined) {
|
|
96
|
+
return undefined;
|
|
97
|
+
}
|
|
98
|
+
return optionalArray(value, 'controlGroups').map((item, index) => {
|
|
99
|
+
const path = `controlGroups[${index}]`;
|
|
100
|
+
const group = expectObject(item, path);
|
|
101
|
+
const contextIds = parseOptionalStringArray(group.contextIds, `${path}.contextIds`);
|
|
102
|
+
const description = parseOptionalString(group.description, `${path}.description`);
|
|
103
|
+
return {
|
|
104
|
+
id: expectString(group.id, `${path}.id`),
|
|
105
|
+
name: expectString(group.name, `${path}.name`),
|
|
106
|
+
role: expectString(group.role, `${path}.role`),
|
|
107
|
+
...(contextIds === undefined ? {} : { contextIds }),
|
|
108
|
+
...(description === undefined ? {} : { description }),
|
|
109
|
+
};
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function parseControlContexts(value: YamlValue | undefined): readonly ControlContext[] | undefined {
|
|
114
|
+
if (value === undefined) {
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
117
|
+
return optionalArray(value, 'controlContexts').map((item, index) => {
|
|
118
|
+
const path = `controlContexts[${index}]`;
|
|
119
|
+
const context = expectObject(item, path);
|
|
120
|
+
const description = parseOptionalString(context.description, `${path}.description`);
|
|
121
|
+
return {
|
|
122
|
+
id: expectString(context.id, `${path}.id`),
|
|
123
|
+
name: expectString(context.name, `${path}.name`),
|
|
124
|
+
role: expectString(context.role, `${path}.role`),
|
|
125
|
+
...(description === undefined ? {} : { description }),
|
|
126
|
+
};
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function parseDeviceInterface(value: YamlValue | undefined): DeviceInterface | undefined {
|
|
131
|
+
if (value === undefined) {
|
|
132
|
+
return undefined;
|
|
133
|
+
}
|
|
134
|
+
const deviceInterface = expectObject(value, 'deviceInterface');
|
|
135
|
+
return {
|
|
136
|
+
controls: optionalArray(deviceInterface.controls, 'deviceInterface.controls').map((item, index) => {
|
|
137
|
+
const path = `deviceInterface.controls[${index}]`;
|
|
138
|
+
const control = expectObject(item, path);
|
|
139
|
+
const groupId = parseOptionalString(control.groupId, `${path}.groupId`);
|
|
140
|
+
const order = parseOptionalNumber(control.order, `${path}.order`);
|
|
141
|
+
const binding = parseOptionalDeviceInterfaceBinding(control.binding, `${path}.binding`);
|
|
142
|
+
const appliesWhen = parseOptionalApplicabilityPredicate(control.appliesWhen, `${path}.appliesWhen`);
|
|
143
|
+
const description = parseOptionalString(control.description, `${path}.description`);
|
|
144
|
+
return {
|
|
145
|
+
id: expectString(control.id, `${path}.id`),
|
|
146
|
+
label: expectString(control.label, `${path}.label`),
|
|
147
|
+
kind: parseDeviceInterfaceControlKind(control.kind, `${path}.kind`),
|
|
148
|
+
role: expectString(control.role, `${path}.role`),
|
|
149
|
+
...(groupId === undefined ? {} : { groupId }),
|
|
150
|
+
...(order === undefined ? {} : { order }),
|
|
151
|
+
...(binding === undefined ? {} : { binding }),
|
|
152
|
+
...(appliesWhen === undefined ? {} : { appliesWhen }),
|
|
153
|
+
...(description === undefined ? {} : { description }),
|
|
154
|
+
};
|
|
155
|
+
}),
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function parseDeviceInterfaceControlKind(
|
|
160
|
+
value: YamlValue | undefined,
|
|
161
|
+
path: string,
|
|
162
|
+
): DeviceInterfaceControlKind {
|
|
163
|
+
const kind = expectString(value, path);
|
|
164
|
+
switch (kind) {
|
|
165
|
+
case 'knob':
|
|
166
|
+
case 'slider':
|
|
167
|
+
case 'switch':
|
|
168
|
+
case 'selector':
|
|
169
|
+
case 'footswitch':
|
|
170
|
+
case 'led':
|
|
171
|
+
case 'jack':
|
|
172
|
+
return kind;
|
|
173
|
+
default:
|
|
174
|
+
throw new Error(`${path}: expected knob, slider, switch, selector, footswitch, led, or jack`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function parseOptionalDeviceInterfaceBinding(
|
|
179
|
+
value: YamlValue | undefined,
|
|
180
|
+
path: string,
|
|
181
|
+
): DeviceInterfaceBinding | undefined {
|
|
182
|
+
if (value === undefined) {
|
|
183
|
+
return undefined;
|
|
184
|
+
}
|
|
185
|
+
const binding = expectObject(value, path);
|
|
186
|
+
const controlId = parseOptionalString(binding.controlId, `${path}.controlId`);
|
|
187
|
+
const controlName = parseOptionalString(binding.controlName, `${path}.controlName`);
|
|
188
|
+
const property = parseOptionalString(binding.property, `${path}.property`);
|
|
189
|
+
const externalInterfaceId = parseOptionalString(binding.externalInterfaceId, `${path}.externalInterfaceId`);
|
|
190
|
+
return {
|
|
191
|
+
componentId: expectString(binding.componentId, `${path}.componentId`),
|
|
192
|
+
...(controlId === undefined ? {} : { controlId }),
|
|
193
|
+
...(controlName === undefined ? {} : { controlName }),
|
|
194
|
+
...(property === undefined ? {} : { property }),
|
|
195
|
+
...(externalInterfaceId === undefined ? {} : { externalInterfaceId }),
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function parseOptionalApplicabilityPredicate(
|
|
200
|
+
value: YamlValue | undefined,
|
|
201
|
+
path: string,
|
|
202
|
+
): ControlApplicabilityPredicate | undefined {
|
|
203
|
+
if (value === undefined) {
|
|
204
|
+
return undefined;
|
|
205
|
+
}
|
|
206
|
+
const predicate = expectObject(value, path);
|
|
207
|
+
const allOf = parseOptionalStringArray(predicate.allOf, `${path}.allOf`);
|
|
208
|
+
const anyOf = parseOptionalStringArray(predicate.anyOf, `${path}.anyOf`);
|
|
209
|
+
return {
|
|
210
|
+
...(allOf === undefined ? {} : { allOf }),
|
|
211
|
+
...(anyOf === undefined ? {} : { anyOf }),
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function parseDevice(value: YamlValue | undefined): CircuitDocumentDevice | undefined {
|
|
216
|
+
if (value === undefined) {
|
|
217
|
+
return undefined;
|
|
218
|
+
}
|
|
219
|
+
const device = expectObject(value, 'device');
|
|
220
|
+
const id = parseOptionalString(device.id, 'device.id');
|
|
221
|
+
const version = parseOptionalPositiveInteger(device.version, 'device.version');
|
|
222
|
+
const family = parseOptionalString(device.family, 'device.family');
|
|
223
|
+
const model = parseOptionalString(device.model, 'device.model');
|
|
224
|
+
const audioProcessing = parseOptionalBoolean(device.audioProcessing, 'device.audioProcessing');
|
|
225
|
+
return {
|
|
226
|
+
...(id === undefined ? {} : { id }),
|
|
227
|
+
...(version === undefined ? {} : { version }),
|
|
228
|
+
kind: parseCircuitDocumentDeviceKind(device.kind, 'device.kind'),
|
|
229
|
+
...(family === undefined ? {} : { family }),
|
|
230
|
+
...(model === undefined ? {} : { model }),
|
|
231
|
+
...(audioProcessing === undefined ? {} : { audioProcessing }),
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function parseCircuitDocumentDeviceKind(value: YamlValue | undefined, path: string): CircuitDocumentDeviceKind {
|
|
236
|
+
const kind = expectString(value, path);
|
|
237
|
+
switch (kind) {
|
|
238
|
+
case 'audio-pedal':
|
|
239
|
+
case 'control-accessory':
|
|
240
|
+
case 'utility':
|
|
241
|
+
case 'unknown':
|
|
242
|
+
return kind;
|
|
243
|
+
default:
|
|
244
|
+
throw new Error(`${path}: expected audio-pedal, control-accessory, utility, or unknown`);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function parseControlOutputs(value: YamlValue | undefined): readonly ControlOutput[] | undefined {
|
|
249
|
+
if (value === undefined) {
|
|
250
|
+
return undefined;
|
|
251
|
+
}
|
|
252
|
+
return optionalArray(value, 'controlOutputs').map((item, index) => {
|
|
253
|
+
const path = `controlOutputs[${index}]`;
|
|
254
|
+
const controlOutput = expectObject(item, path);
|
|
255
|
+
const connector = parseOptionalControlInterfaceConnector(controlOutput.connector, `${path}.connector`);
|
|
256
|
+
const switchMode = parseOptionalControlOutputSwitchMode(controlOutput.switchMode, `${path}.switchMode`);
|
|
257
|
+
const polarity = parseOptionalControlInterfacePolarity(controlOutput.polarity, `${path}.polarity`);
|
|
258
|
+
const inactiveValue = parseOptionalNumber(controlOutput.inactiveValue, `${path}.inactiveValue`);
|
|
259
|
+
const activeValue = parseOptionalNumber(controlOutput.activeValue, `${path}.activeValue`);
|
|
260
|
+
const componentId = parseOptionalString(controlOutput.componentId, `${path}.componentId`);
|
|
261
|
+
const description = parseOptionalString(controlOutput.description, `${path}.description`);
|
|
262
|
+
return {
|
|
263
|
+
id: expectString(controlOutput.id, `${path}.id`),
|
|
264
|
+
name: expectString(controlOutput.name, `${path}.name`),
|
|
265
|
+
role: parseControlInterfaceRole(controlOutput.role, `${path}.role`),
|
|
266
|
+
...(connector === undefined ? {} : { connector }),
|
|
267
|
+
...(switchMode === undefined ? {} : { switchMode }),
|
|
268
|
+
...(polarity === undefined ? {} : { polarity }),
|
|
269
|
+
...(inactiveValue === undefined ? {} : { inactiveValue }),
|
|
270
|
+
...(activeValue === undefined ? {} : { activeValue }),
|
|
271
|
+
...(componentId === undefined ? {} : { componentId }),
|
|
272
|
+
...(description === undefined ? {} : { description }),
|
|
273
|
+
};
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function parseOptionalControlOutputSwitchMode(
|
|
278
|
+
value: YamlValue | undefined,
|
|
279
|
+
path: string,
|
|
280
|
+
): ControlOutputSwitchMode | undefined {
|
|
281
|
+
if (value === undefined) {
|
|
282
|
+
return undefined;
|
|
283
|
+
}
|
|
284
|
+
const switchMode = expectString(value, path);
|
|
285
|
+
switch (switchMode) {
|
|
286
|
+
case 'momentary':
|
|
287
|
+
case 'latching':
|
|
288
|
+
return switchMode;
|
|
289
|
+
default:
|
|
290
|
+
throw new Error(`${path}: expected momentary or latching`);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function parseControlInterfaces(value: YamlValue | undefined): readonly ControlInterface[] | undefined {
|
|
295
|
+
if (value === undefined) {
|
|
296
|
+
return undefined;
|
|
297
|
+
}
|
|
298
|
+
return optionalArray(value, 'controlInterfaces').map((item, index) => {
|
|
299
|
+
const path = `controlInterfaces[${index}]`;
|
|
300
|
+
const controlInterface = expectObject(item, path);
|
|
301
|
+
const componentId = parseOptionalString(controlInterface.componentId, `${path}.componentId`);
|
|
302
|
+
const controlRole = parseOptionalString(controlInterface.controlRole, `${path}.controlRole`);
|
|
303
|
+
const interfaceName = parseOptionalString(controlInterface.interface, `${path}.interface`);
|
|
304
|
+
const connector = parseOptionalControlInterfaceConnector(controlInterface.connector, `${path}.connector`);
|
|
305
|
+
const assignmentHint = parseOptionalControlInterfaceAssignmentHint(
|
|
306
|
+
controlInterface.assignmentHint,
|
|
307
|
+
`${path}.assignmentHint`,
|
|
308
|
+
);
|
|
309
|
+
const polarity = parseOptionalControlInterfacePolarity(controlInterface.polarity, `${path}.polarity`);
|
|
310
|
+
const binding = parseOptionalControlInterfaceBinding(controlInterface.binding, `${path}.binding`);
|
|
311
|
+
const description = parseOptionalString(controlInterface.description, `${path}.description`);
|
|
312
|
+
return {
|
|
313
|
+
id: expectString(controlInterface.id, `${path}.id`),
|
|
314
|
+
name: expectString(controlInterface.name, `${path}.name`),
|
|
315
|
+
role: parseControlInterfaceRole(controlInterface.role, `${path}.role`),
|
|
316
|
+
...(componentId === undefined ? {} : { componentId }),
|
|
317
|
+
...(controlRole === undefined ? {} : { controlRole }),
|
|
318
|
+
...(interfaceName === undefined ? {} : { interface: interfaceName }),
|
|
319
|
+
...(connector === undefined ? {} : { connector }),
|
|
320
|
+
...(assignmentHint === undefined ? {} : { assignmentHint }),
|
|
321
|
+
...(polarity === undefined ? {} : { polarity }),
|
|
322
|
+
...(binding === undefined ? {} : { binding }),
|
|
323
|
+
...(description === undefined ? {} : { description }),
|
|
324
|
+
};
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function parseOptionalControlInterfaceBinding(
|
|
329
|
+
value: YamlValue | undefined,
|
|
330
|
+
path: string,
|
|
331
|
+
): ControlInterface['binding'] | undefined {
|
|
332
|
+
if (value === undefined) {
|
|
333
|
+
return undefined;
|
|
334
|
+
}
|
|
335
|
+
const binding = expectObject(value, path);
|
|
336
|
+
const sourceComponentId = parseOptionalString(binding.sourceComponentId, `${path}.sourceComponentId`);
|
|
337
|
+
const controlId = parseOptionalString(binding.controlId, `${path}.controlId`);
|
|
338
|
+
const controlName = parseOptionalString(binding.controlName, `${path}.controlName`);
|
|
339
|
+
const property = parseOptionalString(binding.property, `${path}.property`);
|
|
340
|
+
return {
|
|
341
|
+
...(sourceComponentId === undefined ? {} : { sourceComponentId }),
|
|
342
|
+
...(controlId === undefined ? {} : { controlId }),
|
|
343
|
+
...(controlName === undefined ? {} : { controlName }),
|
|
344
|
+
...(property === undefined ? {} : { property }),
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function parseControlInterfaceRole(value: YamlValue | undefined, path: string): ControlInterfaceRole {
|
|
349
|
+
const role = expectString(value, path);
|
|
350
|
+
switch (role) {
|
|
351
|
+
case 'external-control':
|
|
352
|
+
case 'tempo-tap':
|
|
353
|
+
case 'trigger':
|
|
354
|
+
case 'reset':
|
|
355
|
+
case 'sampler-trigger':
|
|
356
|
+
case 'expression':
|
|
357
|
+
case 'unknown':
|
|
358
|
+
return role;
|
|
359
|
+
default:
|
|
360
|
+
throw new Error(`${path}: expected external-control, tempo-tap, trigger, reset, sampler-trigger, expression, or unknown`);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function parseOptionalControlInterfaceConnector(
|
|
365
|
+
value: YamlValue | undefined,
|
|
366
|
+
path: string,
|
|
367
|
+
): ControlInterfaceConnector | undefined {
|
|
368
|
+
if (value === undefined) {
|
|
369
|
+
return undefined;
|
|
370
|
+
}
|
|
371
|
+
const connector = expectString(value, path);
|
|
372
|
+
switch (connector) {
|
|
373
|
+
case '1/4-inch-mono-ts':
|
|
374
|
+
case '1/4-inch-trs':
|
|
375
|
+
case '3.5mm-mono-ts':
|
|
376
|
+
case '3.5mm-trs':
|
|
377
|
+
case 'proprietary':
|
|
378
|
+
case 'unknown':
|
|
379
|
+
return connector;
|
|
380
|
+
default:
|
|
381
|
+
throw new Error(`${path}: expected a supported connector kind`);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function parseOptionalControlInterfaceAssignmentHint(
|
|
386
|
+
value: YamlValue | undefined,
|
|
387
|
+
path: string,
|
|
388
|
+
): ControlInterfaceAssignmentHint | undefined {
|
|
389
|
+
if (value === undefined) {
|
|
390
|
+
return undefined;
|
|
391
|
+
}
|
|
392
|
+
const hint = expectString(value, path);
|
|
393
|
+
switch (hint) {
|
|
394
|
+
case 'momentary':
|
|
395
|
+
case 'latching':
|
|
396
|
+
case 'momentary-or-latching':
|
|
397
|
+
case 'continuous':
|
|
398
|
+
return hint;
|
|
399
|
+
default:
|
|
400
|
+
throw new Error(`${path}: expected momentary, latching, momentary-or-latching, or continuous`);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function parseOptionalControlInterfacePolarity(
|
|
405
|
+
value: YamlValue | undefined,
|
|
406
|
+
path: string,
|
|
407
|
+
): ControlInterfacePolarity | undefined {
|
|
408
|
+
if (value === undefined) {
|
|
409
|
+
return undefined;
|
|
410
|
+
}
|
|
411
|
+
const polarity = expectString(value, path);
|
|
412
|
+
switch (polarity) {
|
|
413
|
+
case 'normally-open':
|
|
414
|
+
case 'normally-closed':
|
|
415
|
+
case 'expression':
|
|
416
|
+
case 'unknown':
|
|
417
|
+
return polarity;
|
|
418
|
+
default:
|
|
419
|
+
throw new Error(`${path}: expected normally-open, normally-closed, expression, or unknown`);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function parseYamlSubset(source: string): YamlValue {
|
|
424
|
+
const lines = tokenize(source);
|
|
425
|
+
if (lines.length === 0) {
|
|
426
|
+
throw new Error('interchange YAML is empty');
|
|
427
|
+
}
|
|
428
|
+
const cursor: Cursor = { index: 0 };
|
|
429
|
+
const first = lines[0];
|
|
430
|
+
if (first === undefined) {
|
|
431
|
+
throw new Error('interchange YAML is empty');
|
|
432
|
+
}
|
|
433
|
+
const value = parseBlock(lines, cursor, first.indent);
|
|
434
|
+
if (cursor.index < lines.length) {
|
|
435
|
+
const line = lines[cursor.index];
|
|
436
|
+
throw new Error(`line ${line?.lineNumber ?? cursor.index + 1}: unexpected trailing content`);
|
|
437
|
+
}
|
|
438
|
+
return value;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function tokenize(source: string): readonly YamlLine[] {
|
|
442
|
+
const lines: YamlLine[] = [];
|
|
443
|
+
const rawLines = source.replace(/^/, '').split(/\r?\n/);
|
|
444
|
+
rawLines.forEach((rawLine, index) => {
|
|
445
|
+
if (rawLine.trim().length === 0) {
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
const indentText = rawLine.match(/^\s*/)?.[0] ?? '';
|
|
449
|
+
if (indentText.includes('\t')) {
|
|
450
|
+
throw new Error(`line ${index + 1}: tabs are not supported in interchange YAML`);
|
|
451
|
+
}
|
|
452
|
+
lines.push({
|
|
453
|
+
indent: indentText.length,
|
|
454
|
+
text: rawLine.slice(indentText.length),
|
|
455
|
+
lineNumber: index + 1,
|
|
456
|
+
});
|
|
457
|
+
});
|
|
458
|
+
return lines;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function parseBlock(lines: readonly YamlLine[], cursor: Cursor, indent: number): YamlValue {
|
|
462
|
+
const line = lines[cursor.index];
|
|
463
|
+
if (line === undefined) {
|
|
464
|
+
return {};
|
|
465
|
+
}
|
|
466
|
+
if (line.indent !== indent) {
|
|
467
|
+
throw new Error(`line ${line.lineNumber}: expected indentation ${indent}, got ${line.indent}`);
|
|
468
|
+
}
|
|
469
|
+
if (line.text === '-' || line.text.startsWith('- ')) {
|
|
470
|
+
return parseArray(lines, cursor, indent);
|
|
471
|
+
}
|
|
472
|
+
return parseObject(lines, cursor, indent);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function parseObject(lines: readonly YamlLine[], cursor: Cursor, indent: number): YamlObject {
|
|
476
|
+
const out: YamlObject = {};
|
|
477
|
+
while (cursor.index < lines.length) {
|
|
478
|
+
const line = lines[cursor.index];
|
|
479
|
+
if (line === undefined || line.indent < indent) {
|
|
480
|
+
break;
|
|
481
|
+
}
|
|
482
|
+
if (line.indent > indent) {
|
|
483
|
+
throw new Error(`line ${line.lineNumber}: unexpected indentation ${line.indent}`);
|
|
484
|
+
}
|
|
485
|
+
if (line.text === '-' || line.text.startsWith('- ')) {
|
|
486
|
+
break;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
const pair = parsePair(line.text, line.lineNumber);
|
|
490
|
+
cursor.index += 1;
|
|
491
|
+
out[pair.key] = pair.rest.length > 0
|
|
492
|
+
? parseInlineValue(pair.rest, line.lineNumber)
|
|
493
|
+
: parseNestedValue(lines, cursor, indent, line.lineNumber);
|
|
494
|
+
}
|
|
495
|
+
return out;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function parseArray(lines: readonly YamlLine[], cursor: Cursor, indent: number): readonly YamlValue[] {
|
|
499
|
+
const out: YamlValue[] = [];
|
|
500
|
+
while (cursor.index < lines.length) {
|
|
501
|
+
const line = lines[cursor.index];
|
|
502
|
+
if (line === undefined || line.indent < indent) {
|
|
503
|
+
break;
|
|
504
|
+
}
|
|
505
|
+
if (line.indent > indent) {
|
|
506
|
+
throw new Error(`line ${line.lineNumber}: unexpected indentation ${line.indent}`);
|
|
507
|
+
}
|
|
508
|
+
if (line.text !== '-' && !line.text.startsWith('- ')) {
|
|
509
|
+
break;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
const rest = line.text === '-' ? '' : line.text.slice(2);
|
|
513
|
+
cursor.index += 1;
|
|
514
|
+
if (rest.length === 0) {
|
|
515
|
+
out.push(parseNestedValue(lines, cursor, indent, line.lineNumber));
|
|
516
|
+
} else if (looksLikePair(rest)) {
|
|
517
|
+
out.push(parseObjectItem(rest, lines, cursor, indent + 2, line.lineNumber));
|
|
518
|
+
} else {
|
|
519
|
+
out.push(parseInlineValue(rest, line.lineNumber));
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
return out;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function parseObjectItem(
|
|
526
|
+
firstPairText: string,
|
|
527
|
+
lines: readonly YamlLine[],
|
|
528
|
+
cursor: Cursor,
|
|
529
|
+
indent: number,
|
|
530
|
+
lineNumber: number,
|
|
531
|
+
): YamlObject {
|
|
532
|
+
const out: YamlObject = {};
|
|
533
|
+
const firstPair = parsePair(firstPairText, lineNumber);
|
|
534
|
+
out[firstPair.key] = firstPair.rest.length > 0
|
|
535
|
+
? parseInlineValue(firstPair.rest, lineNumber)
|
|
536
|
+
: parseNestedValue(lines, cursor, indent, lineNumber);
|
|
537
|
+
|
|
538
|
+
while (cursor.index < lines.length) {
|
|
539
|
+
const line = lines[cursor.index];
|
|
540
|
+
if (line === undefined || line.indent < indent) {
|
|
541
|
+
break;
|
|
542
|
+
}
|
|
543
|
+
if (line.indent > indent) {
|
|
544
|
+
throw new Error(`line ${line.lineNumber}: unexpected indentation ${line.indent}`);
|
|
545
|
+
}
|
|
546
|
+
if (line.text === '-' || line.text.startsWith('- ')) {
|
|
547
|
+
break;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
const pair = parsePair(line.text, line.lineNumber);
|
|
551
|
+
cursor.index += 1;
|
|
552
|
+
out[pair.key] = pair.rest.length > 0
|
|
553
|
+
? parseInlineValue(pair.rest, line.lineNumber)
|
|
554
|
+
: parseNestedValue(lines, cursor, indent, line.lineNumber);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
return out;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
function parseNestedValue(
|
|
561
|
+
lines: readonly YamlLine[],
|
|
562
|
+
cursor: Cursor,
|
|
563
|
+
parentIndent: number,
|
|
564
|
+
lineNumber: number,
|
|
565
|
+
): YamlValue {
|
|
566
|
+
const next = lines[cursor.index];
|
|
567
|
+
if (next === undefined || next.indent <= parentIndent) {
|
|
568
|
+
return {};
|
|
569
|
+
}
|
|
570
|
+
if (next.indent !== parentIndent + 2) {
|
|
571
|
+
throw new Error(`line ${next.lineNumber}: expected indentation ${parentIndent + 2} after line ${lineNumber}`);
|
|
572
|
+
}
|
|
573
|
+
return parseBlock(lines, cursor, next.indent);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
function parsePair(text: string, lineNumber: number): ParsedPair {
|
|
577
|
+
const colonIndex = findPairColon(text);
|
|
578
|
+
if (colonIndex <= 0) {
|
|
579
|
+
throw new Error(`line ${lineNumber}: expected key/value pair`);
|
|
580
|
+
}
|
|
581
|
+
const keyText = text.slice(0, colonIndex);
|
|
582
|
+
const restText = text.slice(colonIndex + 1);
|
|
583
|
+
return {
|
|
584
|
+
key: parseKey(keyText, lineNumber),
|
|
585
|
+
rest: restText.startsWith(' ') ? restText.slice(1) : restText,
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
function looksLikePair(text: string): boolean {
|
|
590
|
+
return findPairColon(text) > 0;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
function findPairColon(text: string): number {
|
|
594
|
+
if (text.startsWith('"')) {
|
|
595
|
+
const end = findJsonStringEnd(text);
|
|
596
|
+
return end >= 0 && text[end + 1] === ':' ? end + 1 : -1;
|
|
597
|
+
}
|
|
598
|
+
return text.indexOf(':');
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
function findJsonStringEnd(text: string): number {
|
|
602
|
+
let escaped = false;
|
|
603
|
+
for (let index = 1; index < text.length; index += 1) {
|
|
604
|
+
const char = text[index];
|
|
605
|
+
if (escaped) {
|
|
606
|
+
escaped = false;
|
|
607
|
+
continue;
|
|
608
|
+
}
|
|
609
|
+
if (char === '\\') {
|
|
610
|
+
escaped = true;
|
|
611
|
+
continue;
|
|
612
|
+
}
|
|
613
|
+
if (char === '"') {
|
|
614
|
+
return index;
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
return -1;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
function parseKey(text: string, lineNumber: number): string {
|
|
621
|
+
if (!text.startsWith('"')) {
|
|
622
|
+
return text;
|
|
623
|
+
}
|
|
624
|
+
try {
|
|
625
|
+
const parsed = JSON.parse(text);
|
|
626
|
+
if (typeof parsed === 'string') {
|
|
627
|
+
return parsed;
|
|
628
|
+
}
|
|
629
|
+
} catch {
|
|
630
|
+
// Fall through to the consistent parser error below.
|
|
631
|
+
}
|
|
632
|
+
throw new Error(`line ${lineNumber}: invalid quoted key`);
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
function parseInlineValue(text: string, lineNumber: number): YamlValue {
|
|
636
|
+
if (text === '[]') {
|
|
637
|
+
return [];
|
|
638
|
+
}
|
|
639
|
+
if (text === '{}') {
|
|
640
|
+
return {};
|
|
641
|
+
}
|
|
642
|
+
if (text === 'null') {
|
|
643
|
+
return null;
|
|
644
|
+
}
|
|
645
|
+
if (text === 'true') {
|
|
646
|
+
return true;
|
|
647
|
+
}
|
|
648
|
+
if (text === 'false') {
|
|
649
|
+
return false;
|
|
650
|
+
}
|
|
651
|
+
if (text.startsWith('"')) {
|
|
652
|
+
try {
|
|
653
|
+
const parsed = JSON.parse(text);
|
|
654
|
+
if (typeof parsed === 'string') {
|
|
655
|
+
return parsed;
|
|
656
|
+
}
|
|
657
|
+
} catch {
|
|
658
|
+
// Fall through to the consistent parser error below.
|
|
659
|
+
}
|
|
660
|
+
throw new Error(`line ${lineNumber}: invalid quoted scalar`);
|
|
661
|
+
}
|
|
662
|
+
if (/^[+-]?(?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?$/i.test(text)) {
|
|
663
|
+
return Number(text);
|
|
664
|
+
}
|
|
665
|
+
return text;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
function parseMetadata(value: YamlValue | undefined): CircuitDocument['metadata'] {
|
|
669
|
+
const metadata = optionalObject(value, 'metadata');
|
|
670
|
+
return {
|
|
671
|
+
name: scalarText(metadata.name),
|
|
672
|
+
description: scalarText(metadata.description),
|
|
673
|
+
partNumber: scalarText(metadata.partNumber),
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
function parseSource(value: YamlValue | undefined): DocumentSource {
|
|
678
|
+
return parseStringRecord(value, 'source');
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
function parsePanel(value: YamlValue | undefined): PanelPlacementMetadata | undefined {
|
|
682
|
+
if (value === undefined) {
|
|
683
|
+
return undefined;
|
|
684
|
+
}
|
|
685
|
+
const panel = expectObject(value, 'panel');
|
|
686
|
+
|
|
687
|
+
if (panel.faces !== undefined) {
|
|
688
|
+
return {
|
|
689
|
+
faces: optionalArray(panel.faces, 'panel.faces').map((item, index) => parsePanelFace(item, index)),
|
|
690
|
+
};
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
if (panel.layout === undefined) {
|
|
694
|
+
return undefined;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
const layout = parsePanelLayout(panel.layout, 'panel.layout');
|
|
698
|
+
const elementsValue = panel.controls ?? panel.elements;
|
|
699
|
+
const elementsPath = panel.controls === undefined ? 'panel.elements' : 'panel.controls';
|
|
700
|
+
return {
|
|
701
|
+
faces: [{
|
|
702
|
+
id: 'top',
|
|
703
|
+
layout,
|
|
704
|
+
elements: parsePanelElements(elementsValue, layout, elementsPath),
|
|
705
|
+
}],
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
function parsePanelFace(value: YamlValue, index: number): PanelPlacementMetadata['faces'][number] {
|
|
710
|
+
const path = `panel.faces[${index}]`;
|
|
711
|
+
const face = expectObject(value, path);
|
|
712
|
+
const label = parseOptionalString(face.label, `${path}.label`);
|
|
713
|
+
const layout = parsePanelLayout(face.layout, `${path}.layout`);
|
|
714
|
+
return {
|
|
715
|
+
id: expectString(face.id, `${path}.id`),
|
|
716
|
+
...(label === undefined ? {} : { label }),
|
|
717
|
+
layout,
|
|
718
|
+
elements: parsePanelElements(face.elements, layout, `${path}.elements`),
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
function parsePanelLayout(value: YamlValue | undefined, path: string): PanelGridLayout {
|
|
723
|
+
const layout = expectObject(value, path);
|
|
724
|
+
const rowOrder = parseOptionalPanelRowOrder(layout.rowOrder, `${path}.rowOrder`);
|
|
725
|
+
const columnOrder = parseOptionalPanelColumnOrder(layout.columnOrder, `${path}.columnOrder`);
|
|
726
|
+
return {
|
|
727
|
+
kind: parsePanelLayoutKind(layout.kind, `${path}.kind`),
|
|
728
|
+
rows: expectPositiveInteger(layout.rows, `${path}.rows`),
|
|
729
|
+
columns: expectPositiveInteger(layout.columns, `${path}.columns`),
|
|
730
|
+
indexing: parsePanelGridIndexing(layout.indexing, `${path}.indexing`),
|
|
731
|
+
...(rowOrder === undefined ? {} : { rowOrder }),
|
|
732
|
+
...(columnOrder === undefined ? {} : { columnOrder }),
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
function parsePanelElements(
|
|
737
|
+
value: YamlValue | undefined,
|
|
738
|
+
layout: PanelGridLayout,
|
|
739
|
+
path: string,
|
|
740
|
+
): PanelPlacementMetadata['faces'][number]['elements'] {
|
|
741
|
+
return optionalArray(value, path).map((item, index) => {
|
|
742
|
+
const elementPath = `${path}[${index}]`;
|
|
743
|
+
const element = expectObject(item, elementPath);
|
|
744
|
+
const label = parseOptionalString(element.label, `${elementPath}.label`);
|
|
745
|
+
const interfaceControlId = parseOptionalString(element.interfaceControlId, `${elementPath}.interfaceControlId`);
|
|
746
|
+
return {
|
|
747
|
+
bind: parsePanelElementBinding(element, elementPath),
|
|
748
|
+
kind: parsePanelControlKind(
|
|
749
|
+
element.kind ?? element.controlKind,
|
|
750
|
+
element.kind === undefined && element.controlKind !== undefined
|
|
751
|
+
? `${elementPath}.controlKind`
|
|
752
|
+
: `${elementPath}.kind`,
|
|
753
|
+
),
|
|
754
|
+
grid: parsePanelGridPosition(element.grid, `${elementPath}.grid`, layout),
|
|
755
|
+
...(label === undefined ? {} : { label }),
|
|
756
|
+
...(interfaceControlId === undefined ? {} : { interfaceControlId }),
|
|
757
|
+
};
|
|
758
|
+
});
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
function parsePanelElementBinding(element: YamlObject, path: string): PanelElementBinding {
|
|
762
|
+
if (element.bind !== undefined) {
|
|
763
|
+
const bind = expectObject(element.bind, `${path}.bind`);
|
|
764
|
+
const controlId = parseOptionalString(bind.controlId, `${path}.bind.controlId`);
|
|
765
|
+
const controlName = parseOptionalString(bind.controlName, `${path}.bind.controlName`);
|
|
766
|
+
const property = parseOptionalString(bind.property, `${path}.bind.property`);
|
|
767
|
+
return {
|
|
768
|
+
componentId: expectString(bind.componentId, `${path}.bind.componentId`),
|
|
769
|
+
...(controlId === undefined ? {} : { controlId }),
|
|
770
|
+
...(controlName === undefined ? {} : { controlName }),
|
|
771
|
+
...(property === undefined ? {} : { property }),
|
|
772
|
+
};
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
return {
|
|
776
|
+
componentId: expectString(element.componentId, `${path}.componentId`),
|
|
777
|
+
};
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
function parsePanelGridPosition(
|
|
781
|
+
value: YamlValue | undefined,
|
|
782
|
+
path: string,
|
|
783
|
+
layout: PanelGridLayout,
|
|
784
|
+
): PanelGridPosition {
|
|
785
|
+
const grid = expectObject(value, path);
|
|
786
|
+
const rowSpan = parseOptionalPositiveInteger(grid.rowSpan, `${path}.rowSpan`);
|
|
787
|
+
const columnSpan = parseOptionalPositiveInteger(grid.columnSpan, `${path}.columnSpan`);
|
|
788
|
+
const row = expectNonNegativeInteger(grid.row, `${path}.row`);
|
|
789
|
+
const column = expectNonNegativeInteger(grid.column, `${path}.column`);
|
|
790
|
+
validateGridAxis(row, rowSpan ?? 1, layout.rows, layout.indexing, `${path}.row`, 'row');
|
|
791
|
+
validateGridAxis(column, columnSpan ?? 1, layout.columns, layout.indexing, `${path}.column`, 'column');
|
|
792
|
+
return {
|
|
793
|
+
row,
|
|
794
|
+
column,
|
|
795
|
+
...(rowSpan === undefined ? {} : { rowSpan }),
|
|
796
|
+
...(columnSpan === undefined ? {} : { columnSpan }),
|
|
797
|
+
};
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
function validateGridAxis(
|
|
801
|
+
value: number,
|
|
802
|
+
span: number,
|
|
803
|
+
size: number,
|
|
804
|
+
indexing: PanelGridIndexing,
|
|
805
|
+
path: string,
|
|
806
|
+
axisName: 'row' | 'column',
|
|
807
|
+
): void {
|
|
808
|
+
const min = indexing === 'one-based' ? 1 : 0;
|
|
809
|
+
const occupiedEnd = indexing === 'one-based' ? value + span - 1 : value + span;
|
|
810
|
+
if (value < min || occupiedEnd > size) {
|
|
811
|
+
const maxLabel = indexing === 'one-based' ? String(size) : String(size - 1);
|
|
812
|
+
throw new Error(`${path}: expected ${indexing} ${axisName} coordinate within ${min}..${maxLabel}`);
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
function parsePanelLayoutKind(value: YamlValue | undefined, path: string): 'stompbox-grid' {
|
|
817
|
+
const kind = expectString(value, path);
|
|
818
|
+
if (kind === 'stompbox-grid') {
|
|
819
|
+
return kind;
|
|
820
|
+
}
|
|
821
|
+
throw new Error(`${path}: expected stompbox-grid`);
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
function parsePanelGridIndexing(value: YamlValue | undefined, path: string): PanelGridIndexing {
|
|
825
|
+
const indexing = expectString(value, path);
|
|
826
|
+
if (indexing === 'one-based' || indexing === 'zero-based') {
|
|
827
|
+
return indexing;
|
|
828
|
+
}
|
|
829
|
+
throw new Error(`${path}: expected one-based or zero-based`);
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
function parseOptionalPanelRowOrder(value: YamlValue | undefined, path: string): PanelRowOrder | undefined {
|
|
833
|
+
if (value === undefined) {
|
|
834
|
+
return undefined;
|
|
835
|
+
}
|
|
836
|
+
const order = expectString(value, path);
|
|
837
|
+
if (order === 'top-to-bottom' || order === 'bottom-to-top') {
|
|
838
|
+
return order;
|
|
839
|
+
}
|
|
840
|
+
throw new Error(`${path}: expected top-to-bottom or bottom-to-top`);
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
function parseOptionalPanelColumnOrder(value: YamlValue | undefined, path: string): PanelColumnOrder | undefined {
|
|
844
|
+
if (value === undefined) {
|
|
845
|
+
return undefined;
|
|
846
|
+
}
|
|
847
|
+
const order = expectString(value, path);
|
|
848
|
+
if (order === 'left-to-right' || order === 'right-to-left') {
|
|
849
|
+
return order;
|
|
850
|
+
}
|
|
851
|
+
throw new Error(`${path}: expected left-to-right or right-to-left`);
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
function parsePanelControlKind(value: YamlValue | undefined, path: string): PanelControlKind {
|
|
855
|
+
const kind = expectString(value, path);
|
|
856
|
+
switch (kind) {
|
|
857
|
+
case 'knob':
|
|
858
|
+
case 'slider':
|
|
859
|
+
case 'switch':
|
|
860
|
+
case 'led':
|
|
861
|
+
case 'jack':
|
|
862
|
+
return kind;
|
|
863
|
+
default:
|
|
864
|
+
throw new Error(`${path}: expected knob, slider, switch, led, or jack`);
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
function parseComponents(value: YamlValue | undefined): readonly Component[] {
|
|
869
|
+
return optionalArray(value, 'components').map((item, index) => {
|
|
870
|
+
const path = `components[${index}]`;
|
|
871
|
+
const component = expectObject(item, path);
|
|
872
|
+
return {
|
|
873
|
+
id: expectString(component.id, `${path}.id`),
|
|
874
|
+
kind: parseComponentKind(component.kind, `${path}.kind`),
|
|
875
|
+
name: expectString(component.name, `${path}.name`),
|
|
876
|
+
origin: parsePoint(component.origin, `${path}.origin`),
|
|
877
|
+
rotation: parseRotation(component.rotation, `${path}.rotation`),
|
|
878
|
+
flipped: expectBoolean(component.flipped, `${path}.flipped`),
|
|
879
|
+
terminals: parseTerminals(component.terminals, `${path}.terminals`),
|
|
880
|
+
properties: parseProperties(component.properties, `${path}.properties`),
|
|
881
|
+
sourceTypeName: parseNullableString(component.sourceTypeName, `${path}.sourceTypeName`),
|
|
882
|
+
};
|
|
883
|
+
});
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
function parseTerminals(value: YamlValue | undefined, path: string): readonly Terminal[] {
|
|
887
|
+
return optionalArray(value, path).map((item, index) => {
|
|
888
|
+
const terminalPath = `${path}[${index}]`;
|
|
889
|
+
const terminal = expectObject(item, terminalPath);
|
|
890
|
+
return {
|
|
891
|
+
name: expectString(terminal.name, `${terminalPath}.name`),
|
|
892
|
+
position: parsePoint(terminal.position, `${terminalPath}.position`),
|
|
893
|
+
};
|
|
894
|
+
});
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
function parseProperties(value: YamlValue | undefined, path: string): Readonly<Record<string, PropertyValue>> {
|
|
898
|
+
const properties = optionalObject(value, path);
|
|
899
|
+
const out: Record<string, PropertyValue> = {};
|
|
900
|
+
for (const [key, child] of Object.entries(properties)) {
|
|
901
|
+
out[key] = parsePropertyValue(child, `${path}.${key}`);
|
|
902
|
+
}
|
|
903
|
+
return out;
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
function parsePropertyValue(value: YamlValue, path: string): PropertyValue {
|
|
907
|
+
if (isParsedQuantityValue(value)) {
|
|
908
|
+
return {
|
|
909
|
+
raw: expectString(value.raw, `${path}.raw`),
|
|
910
|
+
value: expectNumber(value.value, `${path}.value`),
|
|
911
|
+
unit: expectString(value.unit, `${path}.unit`),
|
|
912
|
+
};
|
|
913
|
+
}
|
|
914
|
+
if (Array.isArray(value)) {
|
|
915
|
+
return value.map((item, index) => parsePropertyValue(item, `${path}[${index}]`));
|
|
916
|
+
}
|
|
917
|
+
if (isYamlObject(value)) {
|
|
918
|
+
const out: Record<string, PropertyValue> = {};
|
|
919
|
+
for (const [key, child] of Object.entries(value)) {
|
|
920
|
+
out[key] = parsePropertyValue(child, `${path}.${key}`);
|
|
921
|
+
}
|
|
922
|
+
return out;
|
|
923
|
+
}
|
|
924
|
+
if (isScalar(value)) {
|
|
925
|
+
return value;
|
|
926
|
+
}
|
|
927
|
+
throw new Error(`${path}: expected scalar property value or parsed quantity`);
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
function isParsedQuantityValue(value: YamlValue): value is ParsedQuantity {
|
|
931
|
+
return isParsedQuantity(value);
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
function parseWires(value: YamlValue | undefined): readonly Wire[] {
|
|
935
|
+
return optionalArray(value, 'wires').map((item, index) => {
|
|
936
|
+
const path = `wires[${index}]`;
|
|
937
|
+
const wire = expectObject(item, path);
|
|
938
|
+
const points = expectArray(wire.points, `${path}.points`);
|
|
939
|
+
if (points.length !== 2) {
|
|
940
|
+
throw new Error(`${path}.points: expected exactly two points`);
|
|
941
|
+
}
|
|
942
|
+
const first = parsePoint(points[0], `${path}.points[0]`);
|
|
943
|
+
const second = parsePoint(points[1], `${path}.points[1]`);
|
|
944
|
+
return {
|
|
945
|
+
id: expectString(wire.id, `${path}.id`),
|
|
946
|
+
endpoints: [first, second],
|
|
947
|
+
};
|
|
948
|
+
});
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
function parseWarnings(value: YamlValue | undefined): readonly Warning[] {
|
|
952
|
+
return optionalArray(value, 'diagnostics').map((item, index) => {
|
|
953
|
+
const path = `diagnostics[${index}]`;
|
|
954
|
+
const warning = expectObject(item, path);
|
|
955
|
+
const out: Warning = {
|
|
956
|
+
code: expectString(warning.code, `${path}.code`),
|
|
957
|
+
message: expectString(warning.message, `${path}.message`),
|
|
958
|
+
...(warning.componentId === undefined
|
|
959
|
+
? {}
|
|
960
|
+
: { componentId: expectString(warning.componentId, `${path}.componentId`) }),
|
|
961
|
+
...(warning.wireId === undefined
|
|
962
|
+
? {}
|
|
963
|
+
: { wireId: expectString(warning.wireId, `${path}.wireId`) }),
|
|
964
|
+
};
|
|
965
|
+
return out;
|
|
966
|
+
});
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
function parseStringArray(value: YamlValue | undefined, path: string): readonly string[] {
|
|
970
|
+
return optionalArray(value, path).map((item, index) => scalarText(item, `${path}[${index}]`));
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
function parseStringRecord(value: YamlValue | undefined, path: string): Readonly<Record<string, string>> {
|
|
974
|
+
const record = optionalObject(value, path);
|
|
975
|
+
const out: Record<string, string> = {};
|
|
976
|
+
for (const [key, child] of Object.entries(record)) {
|
|
977
|
+
out[key] = scalarText(child, `${path}.${key}`);
|
|
978
|
+
}
|
|
979
|
+
return out;
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
function parsePoint(value: YamlValue | undefined, path: string): Point {
|
|
983
|
+
const point = expectObject(value, path);
|
|
984
|
+
return {
|
|
985
|
+
x: expectNumber(point.x, `${path}.x`),
|
|
986
|
+
y: expectNumber(point.y, `${path}.y`),
|
|
987
|
+
};
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
function parseRotation(value: YamlValue | undefined, path: string): Rotation {
|
|
991
|
+
const rotation = expectNumber(value, path);
|
|
992
|
+
if (rotation === 0 || rotation === 1 || rotation === 2 || rotation === 3) {
|
|
993
|
+
return rotation;
|
|
994
|
+
}
|
|
995
|
+
throw new Error(`${path}: expected rotation 0, 1, 2, or 3`);
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
function parseNullableString(value: YamlValue | undefined, path: string): string | null {
|
|
999
|
+
if (value === null || value === undefined) {
|
|
1000
|
+
return null;
|
|
1001
|
+
}
|
|
1002
|
+
return expectString(value, path);
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
function parseComponentKind(value: YamlValue | undefined, path: string): ComponentKind {
|
|
1006
|
+
const kind = expectString(value, path);
|
|
1007
|
+
switch (kind) {
|
|
1008
|
+
case 'resistor':
|
|
1009
|
+
case 'capacitor':
|
|
1010
|
+
case 'inductor':
|
|
1011
|
+
case 'diode':
|
|
1012
|
+
case 'led':
|
|
1013
|
+
case 'bjt':
|
|
1014
|
+
case 'jfet':
|
|
1015
|
+
case 'mosfet':
|
|
1016
|
+
case 'opamp':
|
|
1017
|
+
case 'ota':
|
|
1018
|
+
case 'triode':
|
|
1019
|
+
case 'pentode':
|
|
1020
|
+
case 'tube-diode':
|
|
1021
|
+
case 'transformer':
|
|
1022
|
+
case 'potentiometer':
|
|
1023
|
+
case 'variable-resistor':
|
|
1024
|
+
case 'switch':
|
|
1025
|
+
case 'optocoupler':
|
|
1026
|
+
case 'voltage-source':
|
|
1027
|
+
case 'current-source':
|
|
1028
|
+
case 'battery':
|
|
1029
|
+
case 'ground':
|
|
1030
|
+
case 'rail':
|
|
1031
|
+
case 'jack':
|
|
1032
|
+
case 'bbd':
|
|
1033
|
+
case 'delay-ic':
|
|
1034
|
+
case 'power-amp':
|
|
1035
|
+
case 'regulator':
|
|
1036
|
+
case 'analog-switch':
|
|
1037
|
+
case 'flipflop':
|
|
1038
|
+
case 'ic':
|
|
1039
|
+
case 'label':
|
|
1040
|
+
case 'named-wire':
|
|
1041
|
+
case 'port':
|
|
1042
|
+
case 'unsupported':
|
|
1043
|
+
return kind;
|
|
1044
|
+
default:
|
|
1045
|
+
throw new Error(`${path}: unsupported component kind "${kind}"`);
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
function optionalObject(value: YamlValue | undefined, path: string): YamlObject {
|
|
1050
|
+
if (value === undefined) {
|
|
1051
|
+
return {};
|
|
1052
|
+
}
|
|
1053
|
+
return expectObject(value, path);
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
function optionalArray(value: YamlValue | undefined, path: string): readonly YamlValue[] {
|
|
1057
|
+
if (value === undefined) {
|
|
1058
|
+
return [];
|
|
1059
|
+
}
|
|
1060
|
+
return expectArray(value, path);
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
function expectObject(value: YamlValue | undefined, path: string): YamlObject {
|
|
1064
|
+
if (isYamlObject(value)) {
|
|
1065
|
+
return value;
|
|
1066
|
+
}
|
|
1067
|
+
throw new Error(`${path}: expected object`);
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
function expectArray(value: YamlValue | undefined, path: string): readonly YamlValue[] {
|
|
1071
|
+
if (Array.isArray(value)) {
|
|
1072
|
+
return value;
|
|
1073
|
+
}
|
|
1074
|
+
throw new Error(`${path}: expected array`);
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
function expectString(value: YamlValue | undefined, path: string): string {
|
|
1078
|
+
if (typeof value === 'string') {
|
|
1079
|
+
return value;
|
|
1080
|
+
}
|
|
1081
|
+
throw new Error(`${path}: expected string`);
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
function expectNumber(value: YamlValue | undefined, path: string): number {
|
|
1085
|
+
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
1086
|
+
return value;
|
|
1087
|
+
}
|
|
1088
|
+
throw new Error(`${path}: expected number`);
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
function expectPositiveInteger(value: YamlValue | undefined, path: string): number {
|
|
1092
|
+
const number = expectNumber(value, path);
|
|
1093
|
+
if (Number.isInteger(number) && number > 0) {
|
|
1094
|
+
return number;
|
|
1095
|
+
}
|
|
1096
|
+
throw new Error(`${path}: expected positive integer`);
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
function expectNonNegativeInteger(value: YamlValue | undefined, path: string): number {
|
|
1100
|
+
const number = expectNumber(value, path);
|
|
1101
|
+
if (Number.isInteger(number) && number >= 0) {
|
|
1102
|
+
return number;
|
|
1103
|
+
}
|
|
1104
|
+
throw new Error(`${path}: expected non-negative integer`);
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
function parseOptionalPositiveInteger(value: YamlValue | undefined, path: string): number | undefined {
|
|
1108
|
+
if (value === undefined) {
|
|
1109
|
+
return undefined;
|
|
1110
|
+
}
|
|
1111
|
+
return expectPositiveInteger(value, path);
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
function parseOptionalNumber(value: YamlValue | undefined, path: string): number | undefined {
|
|
1115
|
+
if (value === undefined) {
|
|
1116
|
+
return undefined;
|
|
1117
|
+
}
|
|
1118
|
+
return expectNumber(value, path);
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
function parseOptionalString(value: YamlValue | undefined, path: string): string | undefined {
|
|
1122
|
+
if (value === undefined) {
|
|
1123
|
+
return undefined;
|
|
1124
|
+
}
|
|
1125
|
+
return expectString(value, path);
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
function parseOptionalStringArray(value: YamlValue | undefined, path: string): readonly string[] | undefined {
|
|
1129
|
+
if (value === undefined) {
|
|
1130
|
+
return undefined;
|
|
1131
|
+
}
|
|
1132
|
+
return expectArray(value, path).map((item, index) => expectString(item, `${path}[${index}]`));
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
function parseOptionalBoolean(value: YamlValue | undefined, path: string): boolean | undefined {
|
|
1136
|
+
if (value === undefined) {
|
|
1137
|
+
return undefined;
|
|
1138
|
+
}
|
|
1139
|
+
return expectBoolean(value, path);
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
function expectBoolean(value: YamlValue | undefined, path: string): boolean {
|
|
1143
|
+
if (typeof value === 'boolean') {
|
|
1144
|
+
return value;
|
|
1145
|
+
}
|
|
1146
|
+
throw new Error(`${path}: expected boolean`);
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
function scalarText(value: YamlValue | undefined, path = 'value'): string {
|
|
1150
|
+
if (value === undefined || value === null) {
|
|
1151
|
+
return '';
|
|
1152
|
+
}
|
|
1153
|
+
if (isScalar(value)) {
|
|
1154
|
+
return String(value);
|
|
1155
|
+
}
|
|
1156
|
+
throw new Error(`${path}: expected scalar`);
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
function isScalar(value: YamlValue): value is YamlScalar {
|
|
1160
|
+
return value === null || typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean';
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
function isYamlObject(value: YamlValue | undefined): value is YamlObject {
|
|
1164
|
+
return value !== undefined && value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
1165
|
+
}
|