@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.
Files changed (99) hide show
  1. package/LICENSE.md +21 -0
  2. package/README.md +6 -0
  3. package/package.json +56 -0
  4. package/src/editor/commands.ts +344 -0
  5. package/src/editor/factory.ts +148 -0
  6. package/src/editor/history.ts +142 -0
  7. package/src/editor/index.ts +11 -0
  8. package/src/editor/layout.ts +207 -0
  9. package/src/formats/circuit-json/serializer.ts +1410 -0
  10. package/src/formats/document.ts +274 -0
  11. package/src/formats/interchange/parser.ts +1165 -0
  12. package/src/formats/interchange/serializer.ts +594 -0
  13. package/src/formats/ltspice/catalog.ts +181 -0
  14. package/src/formats/ltspice/encoding.ts +151 -0
  15. package/src/formats/ltspice/parser.ts +432 -0
  16. package/src/formats/ltspice/serializer.ts +169 -0
  17. package/src/formats/schx/catalog.ts +439 -0
  18. package/src/formats/schx/parser.ts +261 -0
  19. package/src/formats/schx/runtime-descriptors.ts +502 -0
  20. package/src/formats/schx/serializer.ts +211 -0
  21. package/src/formats/schx/transforms.ts +38 -0
  22. package/src/formats/spice/parser.ts +373 -0
  23. package/src/formats/spice/serializer.ts +43 -0
  24. package/src/index.ts +205 -0
  25. package/src/model/connectivity.ts +239 -0
  26. package/src/model/netlist.ts +375 -0
  27. package/src/model/properties.ts +101 -0
  28. package/src/model/quantity.ts +173 -0
  29. package/src/model/types.ts +309 -0
  30. package/src/model/validation.ts +985 -0
  31. package/src/model/wires.ts +86 -0
  32. package/src/panel/extract.ts +878 -0
  33. package/src/panel/index.ts +39 -0
  34. package/src/panel/knobs.ts +70 -0
  35. package/src/panel/protocol.ts +117 -0
  36. package/src/panel/types.ts +180 -0
  37. package/src/preview/bounds.ts +85 -0
  38. package/src/preview/box-layout.ts +24 -0
  39. package/src/preview/colors.ts +43 -0
  40. package/src/preview/hanging.ts +94 -0
  41. package/src/preview/junctions.ts +94 -0
  42. package/src/preview/label-layout.ts +90 -0
  43. package/src/preview/ports.ts +101 -0
  44. package/src/preview/renderable-wires.ts +113 -0
  45. package/src/preview/routing.ts +15 -0
  46. package/src/preview/snap.ts +104 -0
  47. package/src/preview/symbols/analog-switch.svg +17 -0
  48. package/src/preview/symbols/battery.svg +16 -0
  49. package/src/preview/symbols/bbd.svg +21 -0
  50. package/src/preview/symbols/bjt-npn.svg +16 -0
  51. package/src/preview/symbols/bjt-pnp.svg +17 -0
  52. package/src/preview/symbols/capacitor-electrolytic.svg +13 -0
  53. package/src/preview/symbols/capacitor.svg +12 -0
  54. package/src/preview/symbols/current-source.svg +14 -0
  55. package/src/preview/symbols/delay-ic.svg +22 -0
  56. package/src/preview/symbols/diode-schottky.svg +12 -0
  57. package/src/preview/symbols/diode-zener.svg +12 -0
  58. package/src/preview/symbols/diode.svg +13 -0
  59. package/src/preview/symbols/flipflop.svg +20 -0
  60. package/src/preview/symbols/ground.svg +12 -0
  61. package/src/preview/symbols/ic-block.svg +20 -0
  62. package/src/preview/symbols/ic.svg +19 -0
  63. package/src/preview/symbols/inductor.svg +11 -0
  64. package/src/preview/symbols/jack-input.svg +16 -0
  65. package/src/preview/symbols/jack-output.svg +16 -0
  66. package/src/preview/symbols/jfet-junction-n.svg +17 -0
  67. package/src/preview/symbols/jfet-n.svg +17 -0
  68. package/src/preview/symbols/jfet-p.svg +17 -0
  69. package/src/preview/symbols/label.svg +8 -0
  70. package/src/preview/symbols/led.svg +18 -0
  71. package/src/preview/symbols/mosfet-n.svg +21 -0
  72. package/src/preview/symbols/mosfet-p.svg +21 -0
  73. package/src/preview/symbols/named-wire.svg +11 -0
  74. package/src/preview/symbols/opamp.svg +21 -0
  75. package/src/preview/symbols/optocoupler.svg +30 -0
  76. package/src/preview/symbols/ota.svg +20 -0
  77. package/src/preview/symbols/pentode.svg +25 -0
  78. package/src/preview/symbols/photoresistor.svg +19 -0
  79. package/src/preview/symbols/port.svg +8 -0
  80. package/src/preview/symbols/potentiometer.svg +15 -0
  81. package/src/preview/symbols/power-amp.svg +20 -0
  82. package/src/preview/symbols/rail.svg +11 -0
  83. package/src/preview/symbols/regulator.svg +13 -0
  84. package/src/preview/symbols/relay.svg +20 -0
  85. package/src/preview/symbols/resistor.svg +11 -0
  86. package/src/preview/symbols/svg-content.ts +59 -0
  87. package/src/preview/symbols/switch-3pdt.svg +32 -0
  88. package/src/preview/symbols/switch-rotary.svg +23 -0
  89. package/src/preview/symbols/switch-spdt.svg +16 -0
  90. package/src/preview/symbols/switch-spst.svg +14 -0
  91. package/src/preview/symbols/switch-toggle.svg +14 -0
  92. package/src/preview/symbols/transformer.svg +17 -0
  93. package/src/preview/symbols/triode.svg +17 -0
  94. package/src/preview/symbols/tube-diode.svg +13 -0
  95. package/src/preview/symbols/unsupported.svg +8 -0
  96. package/src/preview/symbols/variable-resistor.svg +13 -0
  97. package/src/preview/symbols/voltage-source.svg +15 -0
  98. package/src/preview/symbols.ts +207 -0
  99. package/src/preview/wire-chains.ts +153 -0
@@ -0,0 +1,261 @@
1
+ import { parseQuantity } from '../../model/quantity';
2
+ import type {
3
+ CircuitDocument,
4
+ Component,
5
+ ComponentKind,
6
+ DocumentMetadata,
7
+ ParsedQuantity,
8
+ Point,
9
+ PropertyValue,
10
+ Rotation,
11
+ Warning,
12
+ Wire,
13
+ } from '../../model/types';
14
+ import { splitWiresAtJunctions } from '../../model/wires';
15
+ import { isSchxRuntimeDescriptor, lookupSchxDef, shortenSchxType, type SchxComponentDef } from './catalog';
16
+ import { runtimeDescriptorProperties } from './runtime-descriptors';
17
+ import { mapTerminal, normalizeRotation } from './transforms';
18
+
19
+ const ELEMENT_REGEX = /<Element\b([^>]*?)\/>|<Element\b([^>]*?)>([\s\S]*?)<\/Element>/g;
20
+ const COMPONENT_REGEX = /<Component\b([^>]*?)\/>/;
21
+ const ATTR_REGEX = /([A-Za-z_][A-Za-z0-9_:.-]*)="([^"]*)"/g;
22
+ const ROOT_REGEX = /<Schematic\b([^>]*)>/;
23
+ const COMMENT_REGEX = /<!--[\s\S]*?-->/g;
24
+
25
+ export function parseSchx(xml: string): CircuitDocument {
26
+ const normalized = xml.replace(/^/, '').replace(COMMENT_REGEX, '');
27
+ const rootMatch = normalized.match(ROOT_REGEX);
28
+ if (!rootMatch || rootMatch[1] === undefined) {
29
+ throw new Error('not a LiveSPICE schematic: missing <Schematic> root');
30
+ }
31
+
32
+ const rawAttributes = parseAttributes(rootMatch[1]);
33
+ const metadata = parseMetadata(rawAttributes);
34
+ const warnings: Warning[] = [];
35
+ const components: Component[] = [];
36
+ const wires: Wire[] = [];
37
+ const usedIds = new Map<string, number>();
38
+ let elementIndex = 0;
39
+
40
+ for (const match of normalized.matchAll(ELEMENT_REGEX)) {
41
+ const headAttrs = match[1] ?? match[2] ?? '';
42
+ const body = match[3] ?? '';
43
+ const attrs = parseAttributes(headAttrs);
44
+ const typeName = attrs.Type ?? '';
45
+
46
+ if (typeName.includes('Circuit.Wire')) {
47
+ const wire = parseWireElement(attrs, elementIndex, wires.length, warnings);
48
+ if (wire) {
49
+ wires.push(wire);
50
+ }
51
+ } else if (typeName.includes('Circuit.Symbol')) {
52
+ const component = parseSymbolElement(attrs, body, elementIndex, usedIds, warnings);
53
+ if (component) {
54
+ components.push(component);
55
+ }
56
+ } else if (typeName.length > 0) {
57
+ warnings.push({
58
+ code: 'unsupported-element',
59
+ message: `Element ${elementIndex + 1}: unsupported Element type "${typeName}"`,
60
+ });
61
+ }
62
+
63
+ elementIndex += 1;
64
+ }
65
+
66
+ return {
67
+ metadata,
68
+ components,
69
+ wires: splitWiresAtJunctions(wires),
70
+ directives: [],
71
+ warnings,
72
+ rawAttributes,
73
+ };
74
+ }
75
+
76
+ function parseMetadata(attrs: Readonly<Record<string, string>>): DocumentMetadata {
77
+ return {
78
+ name: attrs.Name ?? '',
79
+ description: attrs.Description ?? '',
80
+ partNumber: attrs.PartNumber ?? '',
81
+ };
82
+ }
83
+
84
+ function parseWireElement(
85
+ attrs: Readonly<Record<string, string>>,
86
+ elementIndex: number,
87
+ wireCount: number,
88
+ warnings: Warning[],
89
+ ): Wire | null {
90
+ const a = parsePoint(attrs.A);
91
+ const b = parsePoint(attrs.B);
92
+ if (a === null || b === null) {
93
+ warnings.push({
94
+ code: 'invalid-wire',
95
+ message: `Element ${elementIndex + 1}: wire missing or malformed endpoints`,
96
+ });
97
+ return null;
98
+ }
99
+ return { id: `wire-${wireCount + 1}`, endpoints: [a, b] };
100
+ }
101
+
102
+ function parseSymbolElement(
103
+ attrs: Readonly<Record<string, string>>,
104
+ body: string,
105
+ elementIndex: number,
106
+ usedIds: Map<string, number>,
107
+ warnings: Warning[],
108
+ ): Component | null {
109
+ const componentMatch = body.match(COMPONENT_REGEX);
110
+ if (!componentMatch || componentMatch[1] === undefined) {
111
+ warnings.push({
112
+ code: 'missing-component',
113
+ message: `Element ${elementIndex + 1}: missing <Component> inside Symbol`,
114
+ });
115
+ return null;
116
+ }
117
+
118
+ const componentAttrs = parseAttributes(componentMatch[1]);
119
+ const fullType = componentAttrs._Type ?? '';
120
+ const shortType = shortenSchxType(fullType);
121
+ const def = lookupSchxDef(shortType);
122
+ const kind = resolveSchxKind(shortType, componentAttrs, def);
123
+ const runtimeDescriptor = isSchxRuntimeDescriptor(shortType);
124
+
125
+ if (def === undefined && fullType.length > 0) {
126
+ warnings.push({
127
+ code: 'unknown-component-type',
128
+ message: `Element ${elementIndex + 1}: unrecognized component type "${shortType}"`,
129
+ });
130
+ }
131
+
132
+ const baseName = componentAttrs.Name ?? componentAttrs.Text ?? shortType;
133
+ const id = uniqueId(baseName, usedIds);
134
+ const origin = parsePoint(attrs.Position) ?? { x: 0, y: 0 };
135
+ const rotation: Rotation = normalizeRotation(Number.parseInt(attrs.Rotation ?? '0', 10));
136
+ const flipped = attrs.Flip === 'true';
137
+
138
+ const propertyEntries = Object.entries(componentAttrs).filter(([k]) => k !== '_Type');
139
+ const properties = runtimeDescriptor
140
+ ? withRuntimeDescriptorProperties(buildProperties(propertyEntries, def), shortType)
141
+ : buildProperties(propertyEntries, def);
142
+
143
+ const terminals = (def?.terminals ?? []).map((terminal) => ({
144
+ name: terminal.name,
145
+ position: mapTerminal(terminal.local, origin, rotation, flipped),
146
+ }));
147
+
148
+ if (runtimeDescriptor) {
149
+ warnings.push({
150
+ code: 'runtime-descriptor-imported',
151
+ message: `${baseName} is an imported runtime descriptor from .schx source data, not a source-visible builder primitive.`,
152
+ componentId: id,
153
+ });
154
+ }
155
+
156
+ return {
157
+ id,
158
+ kind,
159
+ name: baseName,
160
+ origin,
161
+ rotation,
162
+ flipped,
163
+ terminals,
164
+ properties,
165
+ sourceTypeName: sourceTypeNameForComponent(shortType, fullType, runtimeDescriptor),
166
+ };
167
+ }
168
+
169
+ function resolveSchxKind(
170
+ shortType: string,
171
+ componentAttrs: Readonly<Record<string, string>>,
172
+ def: SchxComponentDef | undefined,
173
+ ): ComponentKind {
174
+ if (shortType === 'Diode' && componentAttrs.Type?.toLowerCase() === 'led') {
175
+ return 'led';
176
+ }
177
+ return def?.kind ?? 'unsupported';
178
+ }
179
+
180
+ function buildProperties(
181
+ entries: ReadonlyArray<[string, string]>,
182
+ def: SchxComponentDef | undefined,
183
+ ): Readonly<Record<string, PropertyValue>> {
184
+ const quantityKeys = new Set<string>(def?.quantityProps ?? []);
185
+ const properties: Record<string, PropertyValue> = {};
186
+ for (const [key, value] of entries) {
187
+ if (quantityKeys.has(key)) {
188
+ const parsed: ParsedQuantity | null = parseQuantity(value);
189
+ if (parsed) {
190
+ properties[key] = parsed;
191
+ continue;
192
+ }
193
+ }
194
+ properties[key] = value;
195
+ }
196
+ return properties;
197
+ }
198
+
199
+ function withRuntimeDescriptorProperties(
200
+ properties: Readonly<Record<string, PropertyValue>>,
201
+ shortType: string,
202
+ ): Readonly<Record<string, PropertyValue>> {
203
+ return {
204
+ ...properties,
205
+ RuntimeDescriptor: 'true',
206
+ ...runtimeDescriptorProperties(shortType, properties),
207
+ };
208
+ }
209
+
210
+ function sourceTypeNameForComponent(shortType: string, fullType: string, runtimeDescriptor: boolean): string | null {
211
+ if (runtimeDescriptor) {
212
+ return `Circuit.${shortType}`;
213
+ }
214
+ return fullType.length > 0 ? fullType : null;
215
+ }
216
+
217
+ function parseAttributes(input: string): Readonly<Record<string, string>> {
218
+ const out: Record<string, string> = {};
219
+ for (const match of input.matchAll(ATTR_REGEX)) {
220
+ const name = match[1];
221
+ const value = match[2];
222
+ if (name === undefined || value === undefined) {
223
+ continue;
224
+ }
225
+ out[name] = decodeXmlEntities(value);
226
+ }
227
+ return out;
228
+ }
229
+
230
+ function parsePoint(value: string | undefined): Point | null {
231
+ if (value === undefined || value.length === 0) {
232
+ return null;
233
+ }
234
+ const parts = value.split(',');
235
+ if (parts.length !== 2) {
236
+ return null;
237
+ }
238
+ const x = Number.parseInt(parts[0]?.trim() ?? '', 10);
239
+ const y = Number.parseInt(parts[1]?.trim() ?? '', 10);
240
+ if (!Number.isFinite(x) || !Number.isFinite(y)) {
241
+ return null;
242
+ }
243
+ return { x, y };
244
+ }
245
+
246
+ function uniqueId(baseName: string, usedIds: Map<string, number>): string {
247
+ const sanitized = baseName.replace(/\s+/g, '-').replace(/[^A-Za-z0-9_-]/g, '');
248
+ const base = sanitized.length > 0 ? sanitized : 'component';
249
+ const count = usedIds.get(base) ?? 0;
250
+ usedIds.set(base, count + 1);
251
+ return count === 0 ? base : `${base}-${count + 1}`;
252
+ }
253
+
254
+ function decodeXmlEntities(value: string): string {
255
+ return value
256
+ .replaceAll('&quot;', '"')
257
+ .replaceAll('&apos;', "'")
258
+ .replaceAll('&lt;', '<')
259
+ .replaceAll('&gt;', '>')
260
+ .replaceAll('&amp;', '&');
261
+ }