@pepperhorn/chordl-core 0.3.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 (104) hide show
  1. package/LICENSE +21 -0
  2. package/dist/audio/midi-export.d.ts +23 -0
  3. package/dist/audio/midi-export.js +155 -0
  4. package/dist/audio/midi-export.js.map +1 -0
  5. package/dist/chord-sheet/codec.d.ts +14 -0
  6. package/dist/chord-sheet/codec.js +119 -0
  7. package/dist/chord-sheet/codec.js.map +1 -0
  8. package/dist/chord-sheet/defaults.d.ts +13 -0
  9. package/dist/chord-sheet/defaults.js +48 -0
  10. package/dist/chord-sheet/defaults.js.map +1 -0
  11. package/dist/chord-sheet/index.d.ts +3 -0
  12. package/dist/chord-sheet/index.js +4 -0
  13. package/dist/chord-sheet/index.js.map +1 -0
  14. package/dist/chord-sheet/schema.d.ts +6 -0
  15. package/dist/chord-sheet/schema.js +17 -0
  16. package/dist/chord-sheet/schema.js.map +1 -0
  17. package/dist/config.d.ts +2 -0
  18. package/dist/config.js +15 -0
  19. package/dist/config.js.map +1 -0
  20. package/dist/engine/auto-fingering.d.ts +36 -0
  21. package/dist/engine/auto-fingering.js +344 -0
  22. package/dist/engine/auto-fingering.js.map +1 -0
  23. package/dist/engine/highlight-mapper.d.ts +3 -0
  24. package/dist/engine/highlight-mapper.js +46 -0
  25. package/dist/engine/highlight-mapper.js.map +1 -0
  26. package/dist/engine/keyboard-layout.d.ts +6 -0
  27. package/dist/engine/keyboard-layout.js +65 -0
  28. package/dist/engine/keyboard-layout.js.map +1 -0
  29. package/dist/engine/note-spelling.d.ts +54 -0
  30. package/dist/engine/note-spelling.js +103 -0
  31. package/dist/engine/note-spelling.js.map +1 -0
  32. package/dist/engine/scale-fingering.d.ts +21 -0
  33. package/dist/engine/scale-fingering.js +130 -0
  34. package/dist/engine/scale-fingering.js.map +1 -0
  35. package/dist/engine/staff-constants.d.ts +16 -0
  36. package/dist/engine/staff-constants.js +20 -0
  37. package/dist/engine/staff-constants.js.map +1 -0
  38. package/dist/engine/staff-glyphs.d.ts +17 -0
  39. package/dist/engine/staff-glyphs.js +59 -0
  40. package/dist/engine/staff-glyphs.js.map +1 -0
  41. package/dist/engine/staff-layout.d.ts +28 -0
  42. package/dist/engine/staff-layout.js +238 -0
  43. package/dist/engine/staff-layout.js.map +1 -0
  44. package/dist/engine/svg-constants.d.ts +19 -0
  45. package/dist/engine/svg-constants.js +40 -0
  46. package/dist/engine/svg-constants.js.map +1 -0
  47. package/dist/index.d.ts +42 -0
  48. package/dist/index.js +37 -0
  49. package/dist/index.js.map +1 -0
  50. package/dist/logging.d.ts +70 -0
  51. package/dist/logging.js +24 -0
  52. package/dist/logging.js.map +1 -0
  53. package/dist/parser/natural-language.d.ts +9 -0
  54. package/dist/parser/natural-language.js +406 -0
  55. package/dist/parser/natural-language.js.map +1 -0
  56. package/dist/parser/progression-parser.d.ts +16 -0
  57. package/dist/parser/progression-parser.js +97 -0
  58. package/dist/parser/progression-parser.js.map +1 -0
  59. package/dist/pipeline.d.ts +33 -0
  60. package/dist/pipeline.js +203 -0
  61. package/dist/pipeline.js.map +1 -0
  62. package/dist/progression/form-templates.d.ts +11 -0
  63. package/dist/progression/form-templates.js +105 -0
  64. package/dist/progression/form-templates.js.map +1 -0
  65. package/dist/progression/index.d.ts +5 -0
  66. package/dist/progression/index.js +4 -0
  67. package/dist/progression/index.js.map +1 -0
  68. package/dist/progression/progression-resolver.d.ts +29 -0
  69. package/dist/progression/progression-resolver.js +135 -0
  70. package/dist/progression/progression-resolver.js.map +1 -0
  71. package/dist/progression/roman-numeral.d.ts +14 -0
  72. package/dist/progression/roman-numeral.js +78 -0
  73. package/dist/progression/roman-numeral.js.map +1 -0
  74. package/dist/resolver/auto-layout.d.ts +20 -0
  75. package/dist/resolver/auto-layout.js +195 -0
  76. package/dist/resolver/auto-layout.js.map +1 -0
  77. package/dist/resolver/chord-resolver.d.ts +8 -0
  78. package/dist/resolver/chord-resolver.js +172 -0
  79. package/dist/resolver/chord-resolver.js.map +1 -0
  80. package/dist/resolver/scale-resolver.d.ts +9 -0
  81. package/dist/resolver/scale-resolver.js +45 -0
  82. package/dist/resolver/scale-resolver.js.map +1 -0
  83. package/dist/themes/boomwhacker.d.ts +2 -0
  84. package/dist/themes/boomwhacker.js +35 -0
  85. package/dist/themes/boomwhacker.js.map +1 -0
  86. package/dist/themes/crf.d.ts +19 -0
  87. package/dist/themes/crf.js +34 -0
  88. package/dist/themes/crf.js.map +1 -0
  89. package/dist/themes/index.d.ts +3 -0
  90. package/dist/themes/index.js +25 -0
  91. package/dist/themes/index.js.map +1 -0
  92. package/dist/themes/simple.d.ts +2 -0
  93. package/dist/themes/simple.js +13 -0
  94. package/dist/themes/simple.js.map +1 -0
  95. package/dist/theory/chord-tones.d.ts +56 -0
  96. package/dist/theory/chord-tones.js +215 -0
  97. package/dist/theory/chord-tones.js.map +1 -0
  98. package/dist/theory/degree-labels.d.ts +31 -0
  99. package/dist/theory/degree-labels.js +74 -0
  100. package/dist/theory/degree-labels.js.map +1 -0
  101. package/dist/types.d.ts +123 -0
  102. package/dist/types.js +2 -0
  103. package/dist/types.js.map +1 -0
  104. package/package.json +37 -0
@@ -0,0 +1,344 @@
1
+ import { normalizeToSharps, PC_SEMITONES as NOTE_SEMITONE } from "./note-spelling";
2
+ /** Max playable span for a single hand: a 12th = 19 semitones */
3
+ const MAX_HAND_SPAN = 19;
4
+ /** Max notes a single hand can play */
5
+ const MAX_HAND_NOTES = 5;
6
+ const BLACK_NOTES = new Set(["C#", "D#", "F#", "G#", "A#"]);
7
+ function normalize(note) {
8
+ return normalizeToSharps(note);
9
+ }
10
+ function isBlack(note) {
11
+ return BLACK_NOTES.has(normalize(note));
12
+ }
13
+ /** Ascending semitone interval between two pitch classes (mod 12). */
14
+ function interval(a, b) {
15
+ const sa = NOTE_SEMITONE[normalize(a)];
16
+ const sb = NOTE_SEMITONE[normalize(b)];
17
+ if (sa == null || sb == null)
18
+ return 0;
19
+ return ((sb - sa) % 12 + 12) % 12;
20
+ }
21
+ /** Total ascending span of a note sequence in semitones (mod 12 per step). */
22
+ function totalSpanMod12(notes) {
23
+ let span = 0;
24
+ for (let i = 1; i < notes.length; i++) {
25
+ span += interval(notes[i - 1], notes[i]);
26
+ }
27
+ return span;
28
+ }
29
+ /** Check if a set of notes is playable by one hand using mod-12 intervals. */
30
+ function isOneHandPlayable(notes) {
31
+ return notes.length <= MAX_HAND_NOTES && totalSpanMod12(notes) <= MAX_HAND_SPAN;
32
+ }
33
+ /** Check if a set of MIDI values is playable by one hand. */
34
+ function isOneHandPlayableMidi(midis) {
35
+ if (midis.length > MAX_HAND_NOTES)
36
+ return false;
37
+ if (midis.length <= 1)
38
+ return true;
39
+ const sorted = [...midis].sort((a, b) => a - b);
40
+ return sorted[sorted.length - 1] - sorted[0] <= MAX_HAND_SPAN;
41
+ }
42
+ // ── Fingering patterns ────────────────────────────────────────────
43
+ /** Standard RH fingering patterns by note count. */
44
+ const RH_PATTERNS = {
45
+ 2: [[1, 5], [1, 3], [2, 5]],
46
+ 3: [[1, 3, 5], [1, 2, 5], [1, 2, 4], [1, 3, 4], [2, 3, 5], [1, 2, 3]],
47
+ 4: [[1, 2, 3, 5], [1, 2, 4, 5], [1, 2, 3, 4], [1, 3, 4, 5], [2, 3, 4, 5]],
48
+ 5: [[1, 2, 3, 4, 5]],
49
+ };
50
+ /**
51
+ * Score a candidate fingering for a set of notes (RH perspective).
52
+ * Lower is better.
53
+ */
54
+ function scoreFingering(notes, fingers) {
55
+ let penalty = 0;
56
+ for (let i = 0; i < notes.length; i++) {
57
+ if (fingers[i] === 1 && isBlack(notes[i]) && i !== 0) {
58
+ penalty += 10;
59
+ }
60
+ if (fingers[i] === 5 && isBlack(notes[i])) {
61
+ penalty += 2;
62
+ }
63
+ }
64
+ for (let i = 0; i < notes.length - 1; i++) {
65
+ const semitones = interval(notes[i], notes[i + 1]);
66
+ const fingerGap = Math.abs(fingers[i + 1] - fingers[i]);
67
+ if (semitones >= 5 && fingerGap <= 1) {
68
+ penalty += 5;
69
+ }
70
+ if (semitones <= 1 && fingerGap >= 3) {
71
+ penalty += 3;
72
+ }
73
+ }
74
+ if (fingers[0] !== 1)
75
+ penalty += 1;
76
+ const last = fingers[fingers.length - 1];
77
+ if (last !== 5 && last !== 4)
78
+ penalty += 1;
79
+ return penalty;
80
+ }
81
+ /** Pick the best fingering pattern for a single hand. */
82
+ function fingerOneHand(notes, hand) {
83
+ const n = notes.length;
84
+ if (n === 0)
85
+ return [];
86
+ if (n === 1)
87
+ return [3];
88
+ const patterns = RH_PATTERNS[n];
89
+ if (!patterns)
90
+ return Array(n).fill(3);
91
+ let bestPattern = patterns[0];
92
+ let bestScore = Infinity;
93
+ for (const pattern of patterns) {
94
+ const score = scoreFingering(notes, pattern);
95
+ if (score < bestScore) {
96
+ bestScore = score;
97
+ bestPattern = pattern;
98
+ }
99
+ }
100
+ // LH: mirror via (5+1)-f so 1↔5, 2↔4, 3 stays
101
+ if (hand === "lh") {
102
+ return bestPattern.map((f) => 6 - f);
103
+ }
104
+ return bestPattern;
105
+ }
106
+ /**
107
+ * Assign fingering to chord notes, splitting across hands when needed.
108
+ *
109
+ * @param notes - Pitch classes in voicing order (e.g. ["B", "D#", "F#", ...])
110
+ * @param handHints - Optional per-note hand assignments from voicing library ("LH"/"RH")
111
+ * @param midiValues - Optional MIDI note numbers for accurate pitch ordering and span calculation.
112
+ * When provided, notes are sorted by actual pitch before splitting.
113
+ */
114
+ export function assignFingering(notes, handHints, midiValues) {
115
+ const n = notes.length;
116
+ if (n === 0)
117
+ return { fingering: [], hands: [] };
118
+ // If hand hints are provided (from voicing library), use them directly
119
+ if (handHints && handHints.length === n) {
120
+ return fingerWithHints(notes, handHints);
121
+ }
122
+ // When MIDI values are provided, sort by actual pitch to avoid crossing
123
+ if (midiValues && midiValues.length === n) {
124
+ return assignFingeringByMidi(notes, midiValues);
125
+ }
126
+ // Fallback: pitch-class-only mode (mod 12)
127
+ if (isOneHandPlayable(notes)) {
128
+ const fingers = fingerOneHand(notes, "rh");
129
+ return { fingering: fingers, hands: Array(n).fill("rh") };
130
+ }
131
+ const splitIdx = findSplitPointMod12(notes);
132
+ return splitAndFinger(notes, splitIdx);
133
+ }
134
+ /**
135
+ * MIDI-aware fingering: sorts notes by actual pitch, splits, and maps back
136
+ * to original order.
137
+ */
138
+ function assignFingeringByMidi(notes, midiValues) {
139
+ const n = notes.length;
140
+ // Create index array sorted by MIDI pitch (ascending)
141
+ const sortedIndices = Array.from({ length: n }, (_, i) => i)
142
+ .sort((a, b) => midiValues[a] - midiValues[b]);
143
+ const sortedNotes = sortedIndices.map((i) => notes[i]);
144
+ const sortedMidis = sortedIndices.map((i) => midiValues[i]);
145
+ // Check if playable by one hand using actual MIDI span
146
+ if (isOneHandPlayableMidi(sortedMidis)) {
147
+ const fingers = fingerOneHand(sortedNotes, "rh");
148
+ // Map back to original order
149
+ const fingering = new Array(n);
150
+ const hands = new Array(n);
151
+ sortedIndices.forEach((origIdx, sortIdx) => {
152
+ fingering[origIdx] = fingers[sortIdx];
153
+ hands[origIdx] = "rh";
154
+ });
155
+ return { fingering, hands };
156
+ }
157
+ // Find best split point using actual MIDI distances
158
+ const splitIdx = findSplitPointMidi(sortedNotes, sortedMidis);
159
+ const lhNotes = sortedNotes.slice(0, splitIdx);
160
+ const rhNotes = sortedNotes.slice(splitIdx);
161
+ const lhFingers = fingerOneHand(lhNotes, "lh");
162
+ const rhFingers = fingerOneHand(rhNotes, "rh");
163
+ // Map back to original order
164
+ const fingering = new Array(n);
165
+ const hands = new Array(n);
166
+ sortedIndices.forEach((origIdx, sortIdx) => {
167
+ if (sortIdx < splitIdx) {
168
+ fingering[origIdx] = lhFingers[sortIdx];
169
+ hands[origIdx] = "lh";
170
+ }
171
+ else {
172
+ fingering[origIdx] = rhFingers[sortIdx - splitIdx];
173
+ hands[origIdx] = "rh";
174
+ }
175
+ });
176
+ return { fingering, hands };
177
+ }
178
+ /** Split and finger using sorted note order. */
179
+ function splitAndFinger(notes, splitIdx) {
180
+ const lhNotes = notes.slice(0, splitIdx);
181
+ const rhNotes = notes.slice(splitIdx);
182
+ const lhFingers = fingerOneHand(lhNotes, "lh");
183
+ const rhFingers = fingerOneHand(rhNotes, "rh");
184
+ return {
185
+ fingering: [...lhFingers, ...rhFingers],
186
+ hands: [
187
+ ...Array(lhNotes.length).fill("lh"),
188
+ ...Array(rhNotes.length).fill("rh"),
189
+ ],
190
+ };
191
+ }
192
+ /** Use voicing library hand hints to finger each hand independently. */
193
+ function fingerWithHints(notes, hints) {
194
+ const lhIndices = [];
195
+ const rhIndices = [];
196
+ for (let i = 0; i < notes.length; i++) {
197
+ if (hints[i] === "LH")
198
+ lhIndices.push(i);
199
+ else
200
+ rhIndices.push(i);
201
+ }
202
+ const lhNotes = lhIndices.map((i) => notes[i]);
203
+ const rhNotes = rhIndices.map((i) => notes[i]);
204
+ const lhFingers = fingerOneHand(lhNotes, "lh");
205
+ const rhFingers = fingerOneHand(rhNotes, "rh");
206
+ const fingering = new Array(notes.length);
207
+ const hands = new Array(notes.length);
208
+ lhIndices.forEach((idx, i) => {
209
+ fingering[idx] = lhFingers[i];
210
+ hands[idx] = "lh";
211
+ });
212
+ rhIndices.forEach((idx, i) => {
213
+ fingering[idx] = rhFingers[i];
214
+ hands[idx] = "rh";
215
+ });
216
+ return { fingering, hands };
217
+ }
218
+ /**
219
+ * Find split point using actual MIDI values — guaranteed no hand crossing.
220
+ *
221
+ * Levine-style:
222
+ * - LH gets 1-3 lower notes (root + guide tones)
223
+ * - RH gets upper notes (extensions)
224
+ * - Prefer split at the widest MIDI gap
225
+ */
226
+ /** Largest interval between adjacent notes in a sorted MIDI array. */
227
+ function maxAdjacentStretch(midis) {
228
+ let max = 0;
229
+ for (let i = 1; i < midis.length; i++) {
230
+ max = Math.max(max, midis[i] - midis[i - 1]);
231
+ }
232
+ return max;
233
+ }
234
+ /**
235
+ * Find split point using actual MIDI values — guaranteed no hand crossing.
236
+ *
237
+ * Scoring priorities (highest to lowest):
238
+ * 1. Minimize worst-case stretch — penalize the hand with the biggest
239
+ * inter-note gap. If LH has spare capacity, absorb notes to give RH room.
240
+ * 2. Prefer split at wider MIDI gaps — natural hand separation.
241
+ * 3. Levine-style preferences — LH 1-3 notes, RH 3-4 notes.
242
+ * 4. Balance note count between hands.
243
+ */
244
+ function findSplitPointMidi(sortedNotes, sortedMidis) {
245
+ const n = sortedNotes.length;
246
+ let bestSplit = Math.min(2, n - 1);
247
+ let bestScore = Infinity;
248
+ for (let split = 1; split < n; split++) {
249
+ const lhMidis = sortedMidis.slice(0, split);
250
+ const rhMidis = sortedMidis.slice(split);
251
+ // Both hands must be playable
252
+ if (split > MAX_HAND_NOTES || (n - split) > MAX_HAND_NOTES)
253
+ continue;
254
+ const lhSpan = lhMidis[lhMidis.length - 1] - lhMidis[0];
255
+ const rhSpan = rhMidis[rhMidis.length - 1] - rhMidis[0];
256
+ if (lhSpan > MAX_HAND_SPAN || rhSpan > MAX_HAND_SPAN)
257
+ continue;
258
+ let score = 0;
259
+ const lhCount = split;
260
+ const rhCount = n - split;
261
+ // 1. STRAIN BALANCE — minimize the worst stretch in either hand.
262
+ const lhStretch = maxAdjacentStretch(lhMidis);
263
+ const rhStretch = maxAdjacentStretch(rhMidis);
264
+ const worstStretch = Math.max(lhStretch, rhStretch);
265
+ score += worstStretch * 5;
266
+ // 2. LOAD BALANCE — penalize hands at max capacity (5 notes = no slack).
267
+ // A hand with spare fingers can accommodate stretches more comfortably.
268
+ // Reward splits where both hands have breathing room.
269
+ const lhLoad = lhCount / MAX_HAND_NOTES; // 0.2 to 1.0
270
+ const rhLoad = rhCount / MAX_HAND_NOTES;
271
+ const loadImbalance = Math.abs(lhLoad - rhLoad);
272
+ score += loadImbalance * 8;
273
+ // Extra penalty when either hand is maxed out at 5 notes
274
+ if (lhCount === MAX_HAND_NOTES || rhCount === MAX_HAND_NOTES) {
275
+ score += 4;
276
+ }
277
+ // 3. Prefer wider MIDI gap at the split point — natural hand separation
278
+ const gap = sortedMidis[split] - sortedMidis[split - 1];
279
+ score -= gap * 3;
280
+ // 4. Levine-style: LH with 1-3 notes (root + guide tones)
281
+ if (lhCount >= 1 && lhCount <= 3)
282
+ score -= 4;
283
+ // 5. Prefer RH with 3-4 notes
284
+ if (rhCount >= 3 && rhCount <= 4)
285
+ score -= 2;
286
+ if (score < bestScore) {
287
+ bestScore = score;
288
+ bestSplit = split;
289
+ }
290
+ }
291
+ return bestSplit;
292
+ }
293
+ /** Fallback split point using mod-12 intervals (no MIDI data). */
294
+ function findSplitPointMod12(notes) {
295
+ const n = notes.length;
296
+ let bestSplit = Math.min(2, n - 1);
297
+ let bestScore = Infinity;
298
+ for (let split = 1; split < n; split++) {
299
+ const lhNotes = notes.slice(0, split);
300
+ const rhNotes = notes.slice(split);
301
+ if (split > MAX_HAND_NOTES || (n - split) > MAX_HAND_NOTES)
302
+ continue;
303
+ if (totalSpanMod12(lhNotes) > MAX_HAND_SPAN || totalSpanMod12(rhNotes) > MAX_HAND_SPAN)
304
+ continue;
305
+ let score = 0;
306
+ // Strain balance: penalize the hand with biggest adjacent interval
307
+ let lhMaxGap = 0;
308
+ for (let i = 1; i < split; i++)
309
+ lhMaxGap = Math.max(lhMaxGap, interval(notes[i - 1], notes[i]));
310
+ let rhMaxGap = 0;
311
+ for (let i = split + 1; i < n; i++)
312
+ rhMaxGap = Math.max(rhMaxGap, interval(notes[i - 1], notes[i]));
313
+ score += Math.max(lhMaxGap, rhMaxGap) * 5;
314
+ const gapSemitones = interval(notes[split - 1], notes[split]);
315
+ score -= gapSemitones * 3;
316
+ if (split >= 1 && split <= 3)
317
+ score -= 4;
318
+ if ((n - split) >= 3 && (n - split) <= 4)
319
+ score -= 2;
320
+ score += Math.abs(split - (n - split));
321
+ if (score < bestScore) {
322
+ bestScore = score;
323
+ bestSplit = split;
324
+ }
325
+ }
326
+ return bestSplit;
327
+ }
328
+ // ── Legacy API ────────────────────────────────────────────────────
329
+ /**
330
+ * Compute auto-fingering for a chord voicing (legacy API).
331
+ */
332
+ export function autoFingering(notes, hand = "rh") {
333
+ const n = notes.length;
334
+ if (n === 0)
335
+ return [];
336
+ if (n === 1)
337
+ return [3];
338
+ if (isOneHandPlayable(notes)) {
339
+ return fingerOneHand(notes, hand);
340
+ }
341
+ const result = assignFingering(notes);
342
+ return result.fingering;
343
+ }
344
+ //# sourceMappingURL=auto-fingering.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auto-fingering.js","sourceRoot":"","sources":["../../src/engine/auto-fingering.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,YAAY,IAAI,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAqBnF,iEAAiE;AACjE,MAAM,aAAa,GAAG,EAAE,CAAC;AAEzB,uCAAuC;AACvC,MAAM,cAAc,GAAG,CAAC,CAAC;AAEzB,MAAM,WAAW,GAAG,IAAI,GAAG,CAAC,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC;AAE5D,SAAS,SAAS,CAAC,IAAY;IAC7B,OAAO,iBAAiB,CAAC,IAAI,CAAC,CAAC;AACjC,CAAC;AAED,SAAS,OAAO,CAAC,IAAY;IAC3B,OAAO,WAAW,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC;AAC1C,CAAC;AAED,sEAAsE;AACtE,SAAS,QAAQ,CAAC,CAAS,EAAE,CAAS;IACpC,MAAM,EAAE,GAAG,aAAa,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC;IACvC,MAAM,EAAE,GAAG,aAAa,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC;IACvC,IAAI,EAAE,IAAI,IAAI,IAAI,EAAE,IAAI,IAAI;QAAE,OAAO,CAAC,CAAC;IACvC,OAAO,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,CAAC,GAAG,EAAE,CAAC;AACpC,CAAC;AAED,8EAA8E;AAC9E,SAAS,cAAc,CAAC,KAAe;IACrC,IAAI,IAAI,GAAG,CAAC,CAAC;IACb,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACtC,IAAI,IAAI,QAAQ,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;IAC3C,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,8EAA8E;AAC9E,SAAS,iBAAiB,CAAC,KAAe;IACxC,OAAO,KAAK,CAAC,MAAM,IAAI,cAAc,IAAI,cAAc,CAAC,KAAK,CAAC,IAAI,aAAa,CAAC;AAClF,CAAC;AAED,6DAA6D;AAC7D,SAAS,qBAAqB,CAAC,KAAe;IAC5C,IAAI,KAAK,CAAC,MAAM,GAAG,cAAc;QAAE,OAAO,KAAK,CAAC;IAChD,IAAI,KAAK,CAAC,MAAM,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC;IACnC,MAAM,MAAM,GAAG,CAAC,GAAG,KAAK,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;IAChD,OAAO,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,IAAI,aAAa,CAAC;AAChE,CAAC;AAED,qEAAqE;AAErE,oDAAoD;AACpD,MAAM,WAAW,GAA+B;IAC9C,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IAC3B,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;IACrE,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;IACzE,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;CACrB,CAAC;AAEF;;;GAGG;AACH,SAAS,cAAc,CAAC,KAAe,EAAE,OAAiB;IACxD,IAAI,OAAO,GAAG,CAAC,CAAC;IAEhB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACtC,IAAI,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;YACrD,OAAO,IAAI,EAAE,CAAC;QAChB,CAAC;QACD,IAAI,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;YAC1C,OAAO,IAAI,CAAC,CAAC;QACf,CAAC;IACH,CAAC;IAED,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;QAC1C,MAAM,SAAS,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;QACnD,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC;QAExD,IAAI,SAAS,IAAI,CAAC,IAAI,SAAS,IAAI,CAAC,EAAE,CAAC;YACrC,OAAO,IAAI,CAAC,CAAC;QACf,CAAC;QACD,IAAI,SAAS,IAAI,CAAC,IAAI,SAAS,IAAI,CAAC,EAAE,CAAC;YACrC,OAAO,IAAI,CAAC,CAAC;QACf,CAAC;IACH,CAAC;IAED,IAAI,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC,CAAC;IAEnC,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IACzC,IAAI,IAAI,KAAK,CAAC,IAAI,IAAI,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC,CAAC;IAE3C,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,yDAAyD;AACzD,SAAS,aAAa,CAAC,KAAe,EAAE,IAAU;IAChD,MAAM,CAAC,GAAG,KAAK,CAAC,MAAM,CAAC;IACvB,IAAI,CAAC,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IACvB,IAAI,CAAC,KAAK,CAAC;QAAE,OAAO,CAAC,CAAC,CAAC,CAAC;IAExB,MAAM,QAAQ,GAAG,WAAW,CAAC,CAAC,CAAC,CAAC;IAChC,IAAI,CAAC,QAAQ;QAAE,OAAO,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAEvC,IAAI,WAAW,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC;IAC9B,IAAI,SAAS,GAAG,QAAQ,CAAC;IAEzB,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;QAC/B,MAAM,KAAK,GAAG,cAAc,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;QAC7C,IAAI,KAAK,GAAG,SAAS,EAAE,CAAC;YACtB,SAAS,GAAG,KAAK,CAAC;YAClB,WAAW,GAAG,OAAO,CAAC;QACxB,CAAC;IACH,CAAC;IAED,8CAA8C;IAC9C,IAAI,IAAI,KAAK,IAAI,EAAE,CAAC;QAClB,OAAO,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;IACvC,CAAC;IACD,OAAO,WAAW,CAAC;AACrB,CAAC;AAWD;;;;;;;GAOG;AACH,MAAM,UAAU,eAAe,CAC7B,KAAe,EACf,SAA2B,EAC3B,UAAqB;IAErB,MAAM,CAAC,GAAG,KAAK,CAAC,MAAM,CAAC;IACvB,IAAI,CAAC,KAAK,CAAC;QAAE,OAAO,EAAE,SAAS,EAAE,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC;IAEjD,uEAAuE;IACvE,IAAI,SAAS,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACxC,OAAO,eAAe,CAAC,KAAK,EAAE,SAAS,CAAC,CAAC;IAC3C,CAAC;IAED,wEAAwE;IACxE,IAAI,UAAU,IAAI,UAAU,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC1C,OAAO,qBAAqB,CAAC,KAAK,EAAE,UAAU,CAAC,CAAC;IAClD,CAAC;IAED,2CAA2C;IAC3C,IAAI,iBAAiB,CAAC,KAAK,CAAC,EAAE,CAAC;QAC7B,MAAM,OAAO,GAAG,aAAa,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;QAC3C,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAY,CAAC,EAAE,CAAC;IACpE,CAAC;IAED,MAAM,QAAQ,GAAG,mBAAmB,CAAC,KAAK,CAAC,CAAC;IAC5C,OAAO,cAAc,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC;AACzC,CAAC;AAED;;;GAGG;AACH,SAAS,qBAAqB,CAC5B,KAAe,EACf,UAAoB;IAEpB,MAAM,CAAC,GAAG,KAAK,CAAC,MAAM,CAAC;IAEvB,sDAAsD;IACtD,MAAM,aAAa,GAAG,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC;SACzD,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,CAAC,CAAC,GAAG,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC;IAEjD,MAAM,WAAW,GAAG,aAAa,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;IACvD,MAAM,WAAW,GAAG,aAAa,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC;IAE5D,uDAAuD;IACvD,IAAI,qBAAqB,CAAC,WAAW,CAAC,EAAE,CAAC;QACvC,MAAM,OAAO,GAAG,aAAa,CAAC,WAAW,EAAE,IAAI,CAAC,CAAC;QACjD,6BAA6B;QAC7B,MAAM,SAAS,GAAG,IAAI,KAAK,CAAS,CAAC,CAAC,CAAC;QACvC,MAAM,KAAK,GAAG,IAAI,KAAK,CAAO,CAAC,CAAC,CAAC;QACjC,aAAa,CAAC,OAAO,CAAC,CAAC,OAAO,EAAE,OAAO,EAAE,EAAE;YACzC,SAAS,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;YACtC,KAAK,CAAC,OAAO,CAAC,GAAG,IAAI,CAAC;QACxB,CAAC,CAAC,CAAC;QACH,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC;IAC9B,CAAC;IAED,oDAAoD;IACpD,MAAM,QAAQ,GAAG,kBAAkB,CAAC,WAAW,EAAE,WAAW,CAAC,CAAC;IAC9D,MAAM,OAAO,GAAG,WAAW,CAAC,KAAK,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC;IAC/C,MAAM,OAAO,GAAG,WAAW,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;IAC5C,MAAM,SAAS,GAAG,aAAa,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;IAC/C,MAAM,SAAS,GAAG,aAAa,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;IAE/C,6BAA6B;IAC7B,MAAM,SAAS,GAAG,IAAI,KAAK,CAAS,CAAC,CAAC,CAAC;IACvC,MAAM,KAAK,GAAG,IAAI,KAAK,CAAO,CAAC,CAAC,CAAC;IACjC,aAAa,CAAC,OAAO,CAAC,CAAC,OAAO,EAAE,OAAO,EAAE,EAAE;QACzC,IAAI,OAAO,GAAG,QAAQ,EAAE,CAAC;YACvB,SAAS,CAAC,OAAO,CAAC,GAAG,SAAS,CAAC,OAAO,CAAC,CAAC;YACxC,KAAK,CAAC,OAAO,CAAC,GAAG,IAAI,CAAC;QACxB,CAAC;aAAM,CAAC;YACN,SAAS,CAAC,OAAO,CAAC,GAAG,SAAS,CAAC,OAAO,GAAG,QAAQ,CAAC,CAAC;YACnD,KAAK,CAAC,OAAO,CAAC,GAAG,IAAI,CAAC;QACxB,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC;AAC9B,CAAC;AAED,gDAAgD;AAChD,SAAS,cAAc,CAAC,KAAe,EAAE,QAAgB;IACvD,MAAM,OAAO,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC;IACzC,MAAM,OAAO,GAAG,KAAK,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;IACtC,MAAM,SAAS,GAAG,aAAa,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;IAC/C,MAAM,SAAS,GAAG,aAAa,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;IAE/C,OAAO;QACL,SAAS,EAAE,CAAC,GAAG,SAAS,EAAE,GAAG,SAAS,CAAC;QACvC,KAAK,EAAE;YACL,GAAG,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,IAAY,CAAC;YAC3C,GAAG,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,IAAY,CAAC;SAC5C;KACF,CAAC;AACJ,CAAC;AAED,wEAAwE;AACxE,SAAS,eAAe,CACtB,KAAe,EACf,KAAsB;IAEtB,MAAM,SAAS,GAAa,EAAE,CAAC;IAC/B,MAAM,SAAS,GAAa,EAAE,CAAC;IAC/B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACtC,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK,IAAI;YAAE,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;;YACpC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACzB,CAAC;IAED,MAAM,OAAO,GAAG,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;IAC/C,MAAM,OAAO,GAAG,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;IAC/C,MAAM,SAAS,GAAG,aAAa,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;IAC/C,MAAM,SAAS,GAAG,aAAa,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;IAE/C,MAAM,SAAS,GAAG,IAAI,KAAK,CAAS,KAAK,CAAC,MAAM,CAAC,CAAC;IAClD,MAAM,KAAK,GAAG,IAAI,KAAK,CAAO,KAAK,CAAC,MAAM,CAAC,CAAC;IAC5C,SAAS,CAAC,OAAO,CAAC,CAAC,GAAG,EAAE,CAAC,EAAE,EAAE;QAC3B,SAAS,CAAC,GAAG,CAAC,GAAG,SAAS,CAAC,CAAC,CAAC,CAAC;QAC9B,KAAK,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC;IACpB,CAAC,CAAC,CAAC;IACH,SAAS,CAAC,OAAO,CAAC,CAAC,GAAG,EAAE,CAAC,EAAE,EAAE;QAC3B,SAAS,CAAC,GAAG,CAAC,GAAG,SAAS,CAAC,CAAC,CAAC,CAAC;QAC9B,KAAK,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC;IACpB,CAAC,CAAC,CAAC;IAEH,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC;AAC9B,CAAC;AAED;;;;;;;GAOG;AACH,sEAAsE;AACtE,SAAS,kBAAkB,CAAC,KAAe;IACzC,IAAI,GAAG,GAAG,CAAC,CAAC;IACZ,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACtC,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;IAC/C,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED;;;;;;;;;GASG;AACH,SAAS,kBAAkB,CAAC,WAAqB,EAAE,WAAqB;IACtE,MAAM,CAAC,GAAG,WAAW,CAAC,MAAM,CAAC;IAC7B,IAAI,SAAS,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IACnC,IAAI,SAAS,GAAG,QAAQ,CAAC;IAEzB,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,CAAC,EAAE,KAAK,EAAE,EAAE,CAAC;QACvC,MAAM,OAAO,GAAG,WAAW,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC;QAC5C,MAAM,OAAO,GAAG,WAAW,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;QAEzC,8BAA8B;QAC9B,IAAI,KAAK,GAAG,cAAc,IAAI,CAAC,CAAC,GAAG,KAAK,CAAC,GAAG,cAAc;YAAE,SAAS;QACrE,MAAM,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;QACxD,MAAM,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;QACxD,IAAI,MAAM,GAAG,aAAa,IAAI,MAAM,GAAG,aAAa;YAAE,SAAS;QAE/D,IAAI,KAAK,GAAG,CAAC,CAAC;QAEd,MAAM,OAAO,GAAG,KAAK,CAAC;QACtB,MAAM,OAAO,GAAG,CAAC,GAAG,KAAK,CAAC;QAE1B,iEAAiE;QACjE,MAAM,SAAS,GAAG,kBAAkB,CAAC,OAAO,CAAC,CAAC;QAC9C,MAAM,SAAS,GAAG,kBAAkB,CAAC,OAAO,CAAC,CAAC;QAC9C,MAAM,YAAY,GAAG,IAAI,CAAC,GAAG,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC;QACpD,KAAK,IAAI,YAAY,GAAG,CAAC,CAAC;QAE1B,yEAAyE;QACzE,2EAA2E;QAC3E,yDAAyD;QACzD,MAAM,MAAM,GAAG,OAAO,GAAG,cAAc,CAAC,CAAC,aAAa;QACtD,MAAM,MAAM,GAAG,OAAO,GAAG,cAAc,CAAC;QACxC,MAAM,aAAa,GAAG,IAAI,CAAC,GAAG,CAAC,MAAM,GAAG,MAAM,CAAC,CAAC;QAChD,KAAK,IAAI,aAAa,GAAG,CAAC,CAAC;QAE3B,yDAAyD;QACzD,IAAI,OAAO,KAAK,cAAc,IAAI,OAAO,KAAK,cAAc,EAAE,CAAC;YAC7D,KAAK,IAAI,CAAC,CAAC;QACb,CAAC;QAED,wEAAwE;QACxE,MAAM,GAAG,GAAG,WAAW,CAAC,KAAK,CAAC,GAAG,WAAW,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC;QACxD,KAAK,IAAI,GAAG,GAAG,CAAC,CAAC;QAEjB,0DAA0D;QAC1D,IAAI,OAAO,IAAI,CAAC,IAAI,OAAO,IAAI,CAAC;YAAE,KAAK,IAAI,CAAC,CAAC;QAE7C,8BAA8B;QAC9B,IAAI,OAAO,IAAI,CAAC,IAAI,OAAO,IAAI,CAAC;YAAE,KAAK,IAAI,CAAC,CAAC;QAE7C,IAAI,KAAK,GAAG,SAAS,EAAE,CAAC;YACtB,SAAS,GAAG,KAAK,CAAC;YAClB,SAAS,GAAG,KAAK,CAAC;QACpB,CAAC;IACH,CAAC;IAED,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,kEAAkE;AAClE,SAAS,mBAAmB,CAAC,KAAe;IAC1C,MAAM,CAAC,GAAG,KAAK,CAAC,MAAM,CAAC;IACvB,IAAI,SAAS,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IACnC,IAAI,SAAS,GAAG,QAAQ,CAAC;IAEzB,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,CAAC,EAAE,KAAK,EAAE,EAAE,CAAC;QACvC,MAAM,OAAO,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC;QACtC,MAAM,OAAO,GAAG,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;QAEnC,IAAI,KAAK,GAAG,cAAc,IAAI,CAAC,CAAC,GAAG,KAAK,CAAC,GAAG,cAAc;YAAE,SAAS;QACrE,IAAI,cAAc,CAAC,OAAO,CAAC,GAAG,aAAa,IAAI,cAAc,CAAC,OAAO,CAAC,GAAG,aAAa;YAAE,SAAS;QAEjG,IAAI,KAAK,GAAG,CAAC,CAAC;QAEd,mEAAmE;QACnE,IAAI,QAAQ,GAAG,CAAC,CAAC;QACjB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,EAAE,CAAC,EAAE;YAAE,QAAQ,GAAG,IAAI,CAAC,GAAG,CAAC,QAAQ,EAAE,QAAQ,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAChG,IAAI,QAAQ,GAAG,CAAC,CAAC;QACjB,KAAK,IAAI,CAAC,GAAG,KAAK,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE;YAAE,QAAQ,GAAG,IAAI,CAAC,GAAG,CAAC,QAAQ,EAAE,QAAQ,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QACpG,KAAK,IAAI,IAAI,CAAC,GAAG,CAAC,QAAQ,EAAE,QAAQ,CAAC,GAAG,CAAC,CAAC;QAE1C,MAAM,YAAY,GAAG,QAAQ,CAAC,KAAK,CAAC,KAAK,GAAG,CAAC,CAAC,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC;QAC9D,KAAK,IAAI,YAAY,GAAG,CAAC,CAAC;QAC1B,IAAI,KAAK,IAAI,CAAC,IAAI,KAAK,IAAI,CAAC;YAAE,KAAK,IAAI,CAAC,CAAC;QACzC,IAAI,CAAC,CAAC,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,KAAK,CAAC,IAAI,CAAC;YAAE,KAAK,IAAI,CAAC,CAAC;QACrD,KAAK,IAAI,IAAI,CAAC,GAAG,CAAC,KAAK,GAAG,CAAC,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC;QAEvC,IAAI,KAAK,GAAG,SAAS,EAAE,CAAC;YACtB,SAAS,GAAG,KAAK,CAAC;YAClB,SAAS,GAAG,KAAK,CAAC;QACpB,CAAC;IACH,CAAC;IAED,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,qEAAqE;AAErE;;GAEG;AACH,MAAM,UAAU,aAAa,CAAC,KAAe,EAAE,OAAa,IAAI;IAC9D,MAAM,CAAC,GAAG,KAAK,CAAC,MAAM,CAAC;IACvB,IAAI,CAAC,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IACvB,IAAI,CAAC,KAAK,CAAC;QAAE,OAAO,CAAC,CAAC,CAAC,CAAC;IAExB,IAAI,iBAAiB,CAAC,KAAK,CAAC,EAAE,CAAC;QAC7B,OAAO,aAAa,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;IACpC,CAAC;IAED,MAAM,MAAM,GAAG,eAAe,CAAC,KAAK,CAAC,CAAC;IACtC,OAAO,MAAM,CAAC,SAAS,CAAC;AAC1B,CAAC"}
@@ -0,0 +1,3 @@
1
+ import type { KeyDescriptor, ColorTheme } from "../types";
2
+ export declare function normalizeNote(note: string): string;
3
+ export declare function mapHighlights(keys: KeyDescriptor[], highlightKeys: string[], theme?: ColorTheme): string[];
@@ -0,0 +1,46 @@
1
+ import { DEFAULT_WHITE_FILL, DEFAULT_BLACK_FILL } from "./svg-constants";
2
+ import { normalizeToSharps } from "./note-spelling";
3
+ export function normalizeNote(note) {
4
+ return normalizeToSharps(note);
5
+ }
6
+ /**
7
+ * Normalize a highlight key. Supports octave-qualified format "note:octave"
8
+ * (e.g., "A#:0", "C:1") for multi-octave keyboards.
9
+ */
10
+ function normalizeHighlightKey(key) {
11
+ const colonIdx = key.indexOf(":");
12
+ if (colonIdx !== -1) {
13
+ const note = normalizeNote(key.slice(0, colonIdx));
14
+ const octave = parseInt(key.slice(colonIdx + 1), 10);
15
+ return { note, octave };
16
+ }
17
+ return { note: normalizeNote(key), octave: null };
18
+ }
19
+ export function mapHighlights(keys, highlightKeys, theme) {
20
+ const normalized = highlightKeys.map(normalizeHighlightKey);
21
+ const remaining = [...normalized];
22
+ return keys.map((key) => {
23
+ const keyNote = normalizeNote(key.note);
24
+ const matchIndex = remaining.findIndex((h) => {
25
+ if (h.note !== keyNote)
26
+ return false;
27
+ if (h.octave !== null && h.octave !== key.octave)
28
+ return false;
29
+ return true;
30
+ });
31
+ const isHighlighted = matchIndex !== -1;
32
+ if (isHighlighted) {
33
+ remaining.splice(matchIndex, 1);
34
+ }
35
+ if (theme) {
36
+ return key.isBlack
37
+ ? theme.blackKey(keyNote, isHighlighted)
38
+ : theme.whiteKey(keyNote, isHighlighted);
39
+ }
40
+ if (isHighlighted) {
41
+ return key.isBlack ? "#5a8ab5" : "#a0c6e8";
42
+ }
43
+ return key.isBlack ? DEFAULT_BLACK_FILL : DEFAULT_WHITE_FILL;
44
+ });
45
+ }
46
+ //# sourceMappingURL=highlight-mapper.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"highlight-mapper.js","sourceRoot":"","sources":["../../src/engine/highlight-mapper.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AACzE,OAAO,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAEpD,MAAM,UAAU,aAAa,CAAC,IAAY;IACxC,OAAO,iBAAiB,CAAC,IAAI,CAAC,CAAC;AACjC,CAAC;AAED;;;GAGG;AACH,SAAS,qBAAqB,CAAC,GAAW;IACxC,MAAM,QAAQ,GAAG,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IAClC,IAAI,QAAQ,KAAK,CAAC,CAAC,EAAE,CAAC;QACpB,MAAM,IAAI,GAAG,aAAa,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC,CAAC;QACnD,MAAM,MAAM,GAAG,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,QAAQ,GAAG,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QACrD,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;IAC1B,CAAC;IACD,OAAO,EAAE,IAAI,EAAE,aAAa,CAAC,GAAG,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;AACpD,CAAC;AAED,MAAM,UAAU,aAAa,CAC3B,IAAqB,EACrB,aAAuB,EACvB,KAAkB;IAElB,MAAM,UAAU,GAAG,aAAa,CAAC,GAAG,CAAC,qBAAqB,CAAC,CAAC;IAC5D,MAAM,SAAS,GAAG,CAAC,GAAG,UAAU,CAAC,CAAC;IAElC,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE;QACtB,MAAM,OAAO,GAAG,aAAa,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACxC,MAAM,UAAU,GAAG,SAAS,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,EAAE;YAC3C,IAAI,CAAC,CAAC,IAAI,KAAK,OAAO;gBAAE,OAAO,KAAK,CAAC;YACrC,IAAI,CAAC,CAAC,MAAM,KAAK,IAAI,IAAI,CAAC,CAAC,MAAM,KAAK,GAAG,CAAC,MAAM;gBAAE,OAAO,KAAK,CAAC;YAC/D,OAAO,IAAI,CAAC;QACd,CAAC,CAAC,CAAC;QACH,MAAM,aAAa,GAAG,UAAU,KAAK,CAAC,CAAC,CAAC;QAExC,IAAI,aAAa,EAAE,CAAC;YAClB,SAAS,CAAC,MAAM,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC;QAClC,CAAC;QAED,IAAI,KAAK,EAAE,CAAC;YACV,OAAO,GAAG,CAAC,OAAO;gBAChB,CAAC,CAAC,KAAK,CAAC,QAAQ,CAAC,OAAO,EAAE,aAAa,CAAC;gBACxC,CAAC,CAAC,KAAK,CAAC,QAAQ,CAAC,OAAO,EAAE,aAAa,CAAC,CAAC;QAC7C,CAAC;QAED,IAAI,aAAa,EAAE,CAAC;YAClB,OAAO,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC;QAC7C,CAAC;QAED,OAAO,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,kBAAkB,CAAC,CAAC,CAAC,kBAAkB,CAAC;IAC/D,CAAC,CAAC,CAAC;AACL,CAAC"}
@@ -0,0 +1,6 @@
1
+ import type { Format, WhiteNote, KeyDescriptor } from "../types";
2
+ export declare function computeKeyboard(startFrom?: WhiteNote, size?: number, format?: Format): KeyDescriptor[];
3
+ export declare function computeSvgDimensions(size: number, format?: Format): {
4
+ width: number;
5
+ height: number;
6
+ };
@@ -0,0 +1,65 @@
1
+ import { WHITE_KEY_WIDTH, WHITE_KEY_WIDTH_EXACT, WHITE_KEY_HEIGHT_COMPACT, WHITE_KEY_HEIGHT_EXACT, BLACK_KEY_WIDTH, BLACK_KEY_WIDTH_EXACT, BLACK_KEY_HEIGHT_COMPACT, BLACK_KEY_HEIGHT_EXACT, BLACK_KEY_OFFSETS, BLACK_KEY_OFFSETS_EXACT, WHITE_NOTES_WITH_SHARPS, WHITE_NOTE_ORDER, } from "./svg-constants";
2
+ const SHARP_SUFFIX = {
3
+ C: "C#",
4
+ D: "D#",
5
+ F: "F#",
6
+ G: "G#",
7
+ A: "A#",
8
+ };
9
+ export function computeKeyboard(startFrom = "C", size = 8, format = "compact") {
10
+ const isExact = format === "exact";
11
+ const whiteHeight = isExact ? WHITE_KEY_HEIGHT_EXACT : WHITE_KEY_HEIGHT_COMPACT;
12
+ const blackHeight = isExact ? BLACK_KEY_HEIGHT_EXACT : BLACK_KEY_HEIGHT_COMPACT;
13
+ const whiteW = isExact ? WHITE_KEY_WIDTH_EXACT : WHITE_KEY_WIDTH;
14
+ const blackW = isExact ? BLACK_KEY_WIDTH_EXACT : BLACK_KEY_WIDTH;
15
+ const offsets = isExact ? BLACK_KEY_OFFSETS_EXACT : BLACK_KEY_OFFSETS;
16
+ const keys = [];
17
+ const startIndex = WHITE_NOTE_ORDER.indexOf(startFrom);
18
+ // Offset by half stroke width so left border isn't clipped
19
+ let whiteX = 0.5;
20
+ let relativeOctave = 0;
21
+ for (let i = 0; i < size; i++) {
22
+ const noteIndex = (startIndex + i) % 7;
23
+ const whiteNote = WHITE_NOTE_ORDER[noteIndex];
24
+ // Octave increments each time we cross C
25
+ if (i > 0 && whiteNote === "C") {
26
+ relativeOctave++;
27
+ }
28
+ // Add white key
29
+ keys.push({
30
+ note: whiteNote,
31
+ octave: relativeOctave,
32
+ isBlack: false,
33
+ x: whiteX,
34
+ y: 0,
35
+ width: whiteW,
36
+ height: whiteHeight,
37
+ });
38
+ // Add black key if this white note has one
39
+ if (WHITE_NOTES_WITH_SHARPS.has(whiteNote)) {
40
+ const offset = offsets[whiteNote];
41
+ keys.push({
42
+ note: SHARP_SUFFIX[whiteNote],
43
+ octave: relativeOctave,
44
+ isBlack: true,
45
+ x: whiteX + offset,
46
+ y: 0,
47
+ width: blackW,
48
+ height: blackHeight,
49
+ });
50
+ }
51
+ whiteX += whiteW;
52
+ }
53
+ return keys;
54
+ }
55
+ export function computeSvgDimensions(size, format = "compact") {
56
+ const isExact = format === "exact";
57
+ const keyHeight = isExact ? WHITE_KEY_HEIGHT_EXACT : WHITE_KEY_HEIGHT_COMPACT;
58
+ const whiteW = isExact ? WHITE_KEY_WIDTH_EXACT : WHITE_KEY_WIDTH;
59
+ // Add padding so strokes + rounded corners aren't clipped at edges
60
+ return {
61
+ width: size * whiteW + 1,
62
+ height: keyHeight + 2,
63
+ };
64
+ }
65
+ //# sourceMappingURL=keyboard-layout.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"keyboard-layout.js","sourceRoot":"","sources":["../../src/engine/keyboard-layout.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Consolidated enharmonic spelling logic.
3
+ *
4
+ * Single source of truth for flat↔sharp conversion, key-context-aware
5
+ * note spelling, and pitch class normalization.
6
+ */
7
+ /** Convert flat note names to their sharp equivalents. */
8
+ export declare const FLAT_TO_SHARP: Record<string, string>;
9
+ /** Convert sharp note names to their flat equivalents. */
10
+ export declare const SHARP_TO_FLAT: Record<string, string>;
11
+ /**
12
+ * Keys whose notes should be spelled with flats.
13
+ * Includes both major flat keys and enharmonic equivalents.
14
+ */
15
+ export declare const FLAT_KEYS: Set<string>;
16
+ /**
17
+ * Normalize a pitch class to sharps for internal matching.
18
+ * e.g. "Bb" → "A#", "Db" → "C#", "C" → "C"
19
+ */
20
+ export declare function normalizeToSharps(note: string): string;
21
+ /**
22
+ * Spell a pitch class to match the key context of the given root.
23
+ * Flat-key roots get flat spelling; sharp-key roots keep sharp spelling.
24
+ *
25
+ * e.g. spellForKey("A#", "Bb") → "Bb"
26
+ * spellForKey("Bb", "E") → "A#"
27
+ */
28
+ export declare function spellForKey(pc: string, key: string): string;
29
+ /**
30
+ * Choose enharmonic spelling with explicit flat preference.
31
+ * Used for roman numeral accidentals (bVII → flat spelling regardless of key).
32
+ */
33
+ export declare function spellWithPreference(note: string, key: string, preferFlat?: boolean): string;
34
+ /**
35
+ * Pitch class to semitone value (0-11). Handles both sharps and flats.
36
+ * C=0, C#/Db=1, D=2, D#/Eb=3, E=4, F=5, F#/Gb=6, G=7, G#/Ab=8, A=9, A#/Bb=10, B=11
37
+ */
38
+ export declare const PC_SEMITONES: Record<string, number>;
39
+ /** A3 MIDI value — default floor for scale starting notes. */
40
+ export declare const MIDI_A3 = 57;
41
+ /**
42
+ * Assign ascending octaves to an array of pitch classes.
43
+ * Each note that would descend (lower semitone than previous) bumps the octave.
44
+ * Optionally compacts the result to stay within a maximum span.
45
+ *
46
+ * @param notes - Sharp-normalized pitch classes in voicing/scale order
47
+ * @param baseOctave - Starting octave for the first note
48
+ * @param maxSpan - Maximum semitone span before folding (0 = no limit)
49
+ * @returns Array of { note, octave } pairs
50
+ */
51
+ export declare function assignAscendingOctaves(notes: string[], baseOctave: number, maxSpan?: number): Array<{
52
+ note: string;
53
+ octave: number;
54
+ }>;
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Consolidated enharmonic spelling logic.
3
+ *
4
+ * Single source of truth for flat↔sharp conversion, key-context-aware
5
+ * note spelling, and pitch class normalization.
6
+ */
7
+ /** Convert flat note names to their sharp equivalents. */
8
+ export const FLAT_TO_SHARP = {
9
+ Db: "C#", Eb: "D#", Gb: "F#", Ab: "G#", Bb: "A#",
10
+ };
11
+ /** Convert sharp note names to their flat equivalents. */
12
+ export const SHARP_TO_FLAT = {
13
+ "C#": "Db", "D#": "Eb", "F#": "Gb", "G#": "Ab", "A#": "Bb",
14
+ };
15
+ /**
16
+ * Keys whose notes should be spelled with flats.
17
+ * Includes both major flat keys and enharmonic equivalents.
18
+ */
19
+ export const FLAT_KEYS = new Set([
20
+ "F", "Bb", "Eb", "Ab", "Db", "Gb", "Cb",
21
+ ]);
22
+ /**
23
+ * Normalize a pitch class to sharps for internal matching.
24
+ * e.g. "Bb" → "A#", "Db" → "C#", "C" → "C"
25
+ */
26
+ export function normalizeToSharps(note) {
27
+ return FLAT_TO_SHARP[note] ?? note;
28
+ }
29
+ /**
30
+ * Spell a pitch class to match the key context of the given root.
31
+ * Flat-key roots get flat spelling; sharp-key roots keep sharp spelling.
32
+ *
33
+ * e.g. spellForKey("A#", "Bb") → "Bb"
34
+ * spellForKey("Bb", "E") → "A#"
35
+ */
36
+ export function spellForKey(pc, key) {
37
+ if (FLAT_KEYS.has(key)) {
38
+ return SHARP_TO_FLAT[pc] ?? pc;
39
+ }
40
+ return FLAT_TO_SHARP[pc] ?? pc;
41
+ }
42
+ /**
43
+ * Choose enharmonic spelling with explicit flat preference.
44
+ * Used for roman numeral accidentals (bVII → flat spelling regardless of key).
45
+ */
46
+ export function spellWithPreference(note, key, preferFlat = false) {
47
+ if (preferFlat) {
48
+ return SHARP_TO_FLAT[note] ?? note;
49
+ }
50
+ return spellForKey(note, key);
51
+ }
52
+ // ── Semitone mapping (canonical, replaces duplicates across codebase) ──
53
+ /**
54
+ * Pitch class to semitone value (0-11). Handles both sharps and flats.
55
+ * C=0, C#/Db=1, D=2, D#/Eb=3, E=4, F=5, F#/Gb=6, G=7, G#/Ab=8, A=9, A#/Bb=10, B=11
56
+ */
57
+ export const PC_SEMITONES = {
58
+ C: 0, "C#": 1, Db: 1,
59
+ D: 2, "D#": 3, Eb: 3,
60
+ E: 4, Fb: 4, "E#": 5,
61
+ F: 5, "F#": 6, Gb: 6,
62
+ G: 7, "G#": 8, Ab: 8,
63
+ A: 9, "A#": 10, Bb: 10,
64
+ B: 11, Cb: 11, "B#": 0,
65
+ };
66
+ /** A3 MIDI value — default floor for scale starting notes. */
67
+ export const MIDI_A3 = 57;
68
+ /**
69
+ * Assign ascending octaves to an array of pitch classes.
70
+ * Each note that would descend (lower semitone than previous) bumps the octave.
71
+ * Optionally compacts the result to stay within a maximum span.
72
+ *
73
+ * @param notes - Sharp-normalized pitch classes in voicing/scale order
74
+ * @param baseOctave - Starting octave for the first note
75
+ * @param maxSpan - Maximum semitone span before folding (0 = no limit)
76
+ * @returns Array of { note, octave } pairs
77
+ */
78
+ export function assignAscendingOctaves(notes, baseOctave, maxSpan = 0) {
79
+ if (notes.length === 0)
80
+ return [];
81
+ let octave = baseOctave;
82
+ let prevSemi = PC_SEMITONES[notes[0]] ?? 0;
83
+ const result = notes.map((n, i) => {
84
+ const semi = PC_SEMITONES[n] ?? 0;
85
+ if (i > 0 && semi <= prevSemi) {
86
+ octave++;
87
+ }
88
+ prevSemi = semi;
89
+ return { note: n, octave };
90
+ });
91
+ // Optional compaction: fold notes down if span exceeds limit
92
+ if (maxSpan > 0 && result.length > 1) {
93
+ const baseSemi = (PC_SEMITONES[result[0].note] ?? 0) + result[0].octave * 12;
94
+ for (let i = 1; i < result.length; i++) {
95
+ const noteSemi = (PC_SEMITONES[result[i].note] ?? 0) + result[i].octave * 12;
96
+ if (noteSemi - baseSemi > maxSpan && result[i].octave > result[0].octave) {
97
+ result[i].octave--;
98
+ }
99
+ }
100
+ }
101
+ return result;
102
+ }
103
+ //# sourceMappingURL=note-spelling.js.map