smoosic 1.0.34 → 1.0.35

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 (71) hide show
  1. package/build/html/smoosic.html +1 -0
  2. package/build/smoosic.js +36 -14
  3. package/package.json +1 -1
  4. package/release/html/smoosic.html +4 -3
  5. package/release/smoosic.js +36 -14
  6. package/release/styles/general.css +15 -0
  7. package/src/application/common.ts +20 -0
  8. package/src/application/eventHandler.ts +44 -8
  9. package/src/render/sui/NoteEntryCaret.ts +739 -0
  10. package/src/render/sui/NoteEntryMediator.ts +58 -0
  11. package/src/render/sui/mapper.ts +22 -4
  12. package/src/render/sui/scoreRender.ts +7 -7
  13. package/src/render/sui/scoreViewOperations.ts +46 -0
  14. package/src/render/sui/tracker.ts +93 -47
  15. package/src/render/vex/vxMeasure.ts +2 -1
  16. package/src/render/vex/vxNote.ts +1 -0
  17. package/src/smo/data/music.ts +17 -0
  18. package/src/smo/data/note.ts +3 -1
  19. package/src/smo/data/noteModifiers.ts +2 -0
  20. package/src/smo/data/scoreModifiers.ts +0 -3
  21. package/src/styles/general.css +22 -0
  22. package/src/ui/components/dialogs/scorePreferences.vue +1 -11
  23. package/types/src/application/application.d.ts +102 -102
  24. package/types/src/application/configuration.d.ts +74 -74
  25. package/types/src/application/dynamicInit.d.ts +1 -1
  26. package/types/src/application/eventHandler.d.ts +78 -78
  27. package/types/src/application/exports.d.ts +494 -494
  28. package/types/src/render/audio/oscillator.d.ts +98 -98
  29. package/types/src/render/audio/player.d.ts +141 -141
  30. package/types/src/render/audio/samples.d.ts +56 -56
  31. package/types/src/render/sui/configuration.d.ts +12 -12
  32. package/types/src/render/sui/formatter.d.ts +151 -151
  33. package/types/src/render/sui/layoutDebug.d.ts +43 -43
  34. package/types/src/render/sui/mapper.d.ts +116 -116
  35. package/types/src/render/sui/renderState.d.ts +88 -88
  36. package/types/src/render/sui/scoreRender.d.ts +93 -93
  37. package/types/src/render/sui/scoreView.d.ts +267 -267
  38. package/types/src/render/sui/scoreViewOperations.d.ts +594 -594
  39. package/types/src/render/sui/scroller.d.ts +34 -34
  40. package/types/src/render/sui/svgPageMap.d.ts +318 -318
  41. package/types/src/render/sui/textEdit.d.ts +310 -310
  42. package/types/src/render/vex/vxMeasure.d.ts +95 -95
  43. package/types/src/smo/data/common.d.ts +220 -220
  44. package/types/src/smo/data/measure.d.ts +510 -510
  45. package/types/src/smo/data/measureModifiers.d.ts +506 -506
  46. package/types/src/smo/data/scoreModifiers.d.ts +433 -433
  47. package/types/src/smo/xform/selections.d.ts +153 -153
  48. package/types/src/ui/common.d.ts +45 -45
  49. package/types/src/ui/configuration.d.ts +31 -31
  50. package/types/src/ui/dialogs/chordChange.d.ts +35 -35
  51. package/types/src/ui/dialogs/components/baseComponent.d.ts +158 -158
  52. package/types/src/ui/dialogs/components/button.d.ts +54 -54
  53. package/types/src/ui/dialogs/components/dropdown.d.ts +78 -78
  54. package/types/src/ui/dialogs/components/pitch.d.ts +95 -95
  55. package/types/src/ui/dialogs/components/rocker.d.ts +66 -66
  56. package/types/src/ui/dialogs/components/textInPlace.d.ts +90 -90
  57. package/types/src/ui/dialogs/components/textInput.d.ts +58 -58
  58. package/types/src/ui/dialogs/components/toggle.d.ts +53 -53
  59. package/types/src/ui/dialogs/dialog.d.ts +201 -201
  60. package/types/src/ui/dialogs/endings.d.ts +61 -61
  61. package/types/src/ui/dialogs/lyric.d.ts +39 -39
  62. package/types/src/ui/dialogs/measureFormat.d.ts +52 -52
  63. package/types/src/ui/dialogs/textBlock.d.ts +61 -61
  64. package/types/src/ui/exceptions.d.ts +12 -12
  65. package/types/src/ui/menus/manager.d.ts +57 -57
  66. package/types/src/ui/navigation.d.ts +15 -15
  67. package/types/typedoc.d.ts +158 -158
  68. package/release/styles/styles.css +0 -0
  69. package/src/styles/fonts/Roboto-Italic-VariableFont_wdth,wght.ttf +0 -0
  70. package/src/styles/fonts/Roboto-VariableFont_wdth,wght.ttf +0 -0
  71. package/src/styles/styles.css +0 -0
@@ -0,0 +1,58 @@
1
+ import {SmoSelection} from "../../smo/xform/selections";
2
+ import {SmoGraceNote} from "../../smo/data/noteModifiers";
3
+ import {Pitch} from "../../smo/data/common";
4
+ import {SuiTracker} from "./tracker";
5
+ import {NoteEntryCaret} from "./NoteEntryCaret";
6
+ import {SuiScoreViewOperations} from "./scoreViewOperations";
7
+
8
+ export interface TrackerDelegate {
9
+ onPitchIndexChanged(pitchIndex: number): void;
10
+ onSingleNoteHighlighted(selection: SmoSelection, graceNote: SmoGraceNote | null): void;
11
+ }
12
+
13
+ export interface CaretDelegate {
14
+ onPitchClicked(pitchIndex: number): void;
15
+ onPitchesChanged(newPitches: Pitch[]): void;
16
+ }
17
+
18
+ export class NoteEntryMediator implements TrackerDelegate, CaretDelegate {
19
+
20
+ constructor(
21
+ private tracker: SuiTracker,
22
+ private caret: NoteEntryCaret,
23
+ private view: SuiScoreViewOperations
24
+ ) {
25
+ this.tracker.delegate = this;
26
+ this.caret.delegate = this;
27
+ }
28
+
29
+ /*
30
+ * TrackerDelegate
31
+ */
32
+ public onPitchIndexChanged(pitchIndex: number) {
33
+ this.caret.renderPitchHighlight(pitchIndex);
34
+ }
35
+
36
+ public onSingleNoteHighlighted(selection: SmoSelection, graceNote: SmoGraceNote | null) {
37
+ this.caret.setSelection(selection, graceNote);
38
+ this.caret.render();
39
+ }
40
+
41
+ /*
42
+ * CaretDelegate
43
+ */
44
+ public onPitchClicked(pitchIndex: number) {
45
+ const currentIndex = this.tracker.getPitchIndex();
46
+ if (currentIndex === pitchIndex) {
47
+ this.tracker.clearPitchIndex();
48
+ } else {
49
+ this.tracker.setPitchIndex(pitchIndex);
50
+ }
51
+ }
52
+
53
+ public onPitchesChanged(newPitches: Pitch[]) {
54
+ this.view.setPitches(newPitches);
55
+ this.tracker.pitchIndex = -1;
56
+ }
57
+
58
+ }
@@ -12,6 +12,8 @@ import { SvgBox } from '../../smo/data/common';
12
12
  import { SmoNote } from '../../smo/data/note';
13
13
  import { SmoScore, SmoModifier } from '../../smo/data/score';
14
14
  import { SvgPageMap } from './svgPageMap';
15
+ import { VxMeasure } from '../vex/vxMeasure';
16
+ import {SuiScoreRender} from "./scoreRender";
15
17
 
16
18
  /**
17
19
  * DI information about renderer, so we can notify renderer and it can contain
@@ -32,7 +34,8 @@ export interface SuiRendererBase {
32
34
  passState: number,
33
35
  renderPromise(): Promise<any>,
34
36
  addToReplaceQueue(mm: SmoSelection[]): void,
35
- renderElement: Element
37
+ renderElement: Element,
38
+ renderer: SuiScoreRender
36
39
  }
37
40
  /**
38
41
  * used to perform highlights in the backgroundd
@@ -68,6 +71,7 @@ export abstract class SuiMapper {
68
71
  selectionRects: Record<number, OutlineInfo[]> = {};
69
72
  outlines: Record<string, OutlineInfo> = {};
70
73
  mapping: boolean = false;
74
+
71
75
  constructor(renderer: SuiRendererBase, scroller: SuiScroller) {
72
76
  // renderer renders the music when it changes
73
77
  this.renderer = renderer;
@@ -637,19 +641,33 @@ export abstract class SuiMapper {
637
641
  // const artifacts = SvgHelpers.findIntersectingArtifactFromMap(bb, this.measureNoteMap, SvgHelpers.smoBox(this.scroller.scrollState.scroll));
638
642
  // TODO: handle overlapping suggestions
639
643
  const modifiers = this.renderer.pageMap.findModifierTabs(logicalBox);
644
+
640
645
  if (modifiers.length) {
646
+ // this.eraseMousePositionBox();
641
647
  this._setModifierAsSuggestion(modifiers[0]);
642
- this.eraseMousePositionBox();
643
648
  } else if (artifacts.length) {
644
649
  const artifact = artifacts[0];
645
- this.eraseMousePositionBox();
650
+ // this.eraseMousePositionBox();
646
651
  this._setArtifactAsSuggestion(artifact);
647
652
  } else {
648
653
  // no intersection, show mouse hint
649
- this.createMousePositionBox(logicalBox);
654
+ // this.createMousePositionBox(logicalBox);
650
655
  }
651
656
  }
652
657
  }
658
+
659
+ getIntersectingArtifact(bb: SvgBox): SmoSelection | null {
660
+ const scrollState = this.scroller.scrollState;
661
+ bb = SvgHelpers.boxPoints(bb.x + scrollState.x, bb.y + scrollState.y, bb.width ? bb.width : 1, bb.height ? bb.height : 1);
662
+ const logicalBox = this.renderer.pageMap.clientToSvg(bb);
663
+ const { selections, page } = this.renderer.pageMap.findArtifact(logicalBox);
664
+ if (page && selections.length) {
665
+ return selections[0];
666
+ }
667
+
668
+ return null
669
+ }
670
+
653
671
  _getRectangleChain(selection: SmoSelection) {
654
672
  const rv: number[] = [];
655
673
  if (!selection.note) {
@@ -259,14 +259,14 @@ export class SuiScoreRender {
259
259
  modifier.logicalBox = pageContext.offsetBbox(modifier.element);
260
260
  }
261
261
  });
262
- }
263
- // unit test codes don't have tracker.
264
- if (this.measureMapper) {
265
- const tmpStaff: SmoSystemStaff | undefined = this.score!.staves.find((ss) => ss.staffId === smoMeasure.measureNumber.staffId);
266
- if (tmpStaff) {
267
- this.measureMapper.mapMeasure(tmpStaff, smoMeasure, printing);
262
+ // unit test codes don't have tracker.
263
+ if (this.measureMapper) {
264
+ const tmpStaff: SmoSystemStaff | undefined = this.score!.staves.find((ss) => ss.staffId === smoMeasure.measureNumber.staffId);
265
+ if (tmpStaff) {
266
+ this.measureMapper.mapMeasure(tmpStaff, smoMeasure, printing);
267
+ }
268
268
  }
269
- }
269
+ }
270
270
  });
271
271
  modifiers.forEach((modifier) => {
272
272
  if (modifier.element) {
@@ -35,6 +35,7 @@ import { SuiPiano } from './piano';
35
35
  import { SvgHelpers } from './svgHelpers';
36
36
  import { PromiseHelpers } from '../../common/promiseHelpers';
37
37
  import { parseJsonText } from 'typescript';
38
+ import {NoteEntryCaret} from "./NoteEntryCaret";
38
39
  declare var $: any;
39
40
  declare var SmoConfig: SmoRenderConfiguration;
40
41
 
@@ -52,6 +53,10 @@ declare var SmoConfig: SmoRenderConfiguration;
52
53
  * @category SuiRender
53
54
  */
54
55
  export class SuiScoreViewOperations extends SuiScoreView {
56
+ constructor(config: SmoRenderConfiguration, svgContainer: HTMLElement, score: SmoScore, scrollSelector: HTMLElement, undoBuffer: UndoBuffer) {
57
+ super(config, svgContainer, score, scrollSelector, undoBuffer);
58
+ }
59
+
55
60
  /**
56
61
  * Add a new text group to the score
57
62
  * @param textGroup a new text group
@@ -348,6 +353,17 @@ export class SuiScoreViewOperations extends SuiScoreView {
348
353
 
349
354
  const altSel = this._getEquivalentSelection(sel);
350
355
 
356
+ // If this is a chord and a specific pitch is selected, just remove that pitch
357
+ const pitchIndex = this.tracker.getPitchIndex();
358
+ if (sel.note.pitches.length > 1 && pitchIndex >= 0 && pitchIndex < sel.note.pitches.length) {
359
+ sel.note.pitches.splice(pitchIndex, 1);
360
+ altSel!.note!.pitches.splice(pitchIndex, 1);
361
+ SmoNote.sortPitches(sel.note);
362
+ SmoNote.sortPitches(altSel!.note! as SmoNote);
363
+ this.tracker.clearPitchIndex();
364
+ return; // skip rest/hide logic for this selection
365
+ }
366
+
351
367
  // set the pitch to be a good position for the rest
352
368
  const pitch = JSON.parse(JSON.stringify(
353
369
  SmoMeasure.defaultPitchForClef[sel.measure.clef]));
@@ -1207,6 +1223,36 @@ export class SuiScoreViewOperations extends SuiScoreView {
1207
1223
  await promise;
1208
1224
  }
1209
1225
 
1226
+ async setPitches(pitches: Pitch[]): Promise<void> {
1227
+ const selections = this.tracker.selections;
1228
+ const measureSelections = this.undoTrackerMeasureSelections('set pitches');
1229
+ const graceNotes = this.tracker.getSelectedGraceNotes();
1230
+ if (graceNotes.length === 1 && graceNotes[0].selection !== null && graceNotes[0].selection?.note !== null) {
1231
+ const grace1 = graceNotes[0].modifier as SmoGraceNote;
1232
+ const index = graceNotes[0].selection.note.graceNotes.findIndex((x) => x.attrs.id === grace1.attrs.id);
1233
+
1234
+ const altSel = this._getEquivalentSelection(graceNotes[0].selection);
1235
+ if (altSel && altSel.note !== null) {
1236
+ const grace2 = altSel.note.graceNotes[index];
1237
+ grace2.pitches = pitches;
1238
+ }
1239
+ grace1.pitches = pitches;
1240
+ } else if( selections.length === 1) {
1241
+ const selected = selections[0];
1242
+ SmoOperation.setPitch(selected, pitches);
1243
+ const altSel = this._getEquivalentSelection(selected);
1244
+ SmoOperation.setPitch(altSel!, pitches);
1245
+ if (this.score.preferences.autoAdvance) {
1246
+ // Don't play the next note and the added pitch at the same time.
1247
+ this.tracker.deferNextAutoPlay();
1248
+ this.tracker.moveSelectionRight();
1249
+ }
1250
+ SuiOscillator.playSelectionNow(selected, this.score, 1);
1251
+ }
1252
+
1253
+ this._renderChangedMeasures(measureSelections);
1254
+ await this.renderer.updatePromise();
1255
+ }
1210
1256
  /**
1211
1257
  * Add a pitch to the score at the cursor. This tries to find the best pitch
1212
1258
  * to match the letter key (F vs F# for instance) based on key and surrounding notes
@@ -6,14 +6,22 @@ import { SmoSelection, SmoSelector, ModifierTab } from '../../smo/xform/selectio
6
6
  import { smoSerialize } from '../../common/serializationHelpers';
7
7
  import { SuiOscillator } from '../audio/oscillator';
8
8
  import { SmoScore } from '../../smo/data/score';
9
- import { SvgBox, KeyEvent, defaultKeyEvent, keyHandler } from '../../smo/data/common';
9
+ import { SvgBox, KeyEvent, defaultKeyEvent, keyHandler, Ticks, Pitch, PitchLetter } from '../../smo/data/common';
10
+ import { SmoMusic } from '../../smo/data/music';
10
11
  import { SuiScroller } from './scroller';
11
12
  import { PasteBuffer } from '../../smo/xform/copypaste';
12
13
  import { SmoNote } from '../../smo/data/note';
13
14
  import { SmoMeasure } from '../../smo/data/measure';
14
15
  import { layoutDebug } from './layoutDebug';
16
+ import { SvgPage } from './svgPageMap';
17
+ import {SmoOperation} from "../../smo/xform/operations";
18
+ import {NoteEntryCaret} from "./NoteEntryCaret";
19
+ import {SmoGraceNote} from "../../smo/data/noteModifiers";
20
+ import {TrackerDelegate} from "./NoteEntryMediator";
15
21
  declare var $: any;
16
22
 
23
+
24
+
17
25
  export interface TrackerKeyHandler {
18
26
  moveHome : keyHandler,
19
27
  moveEnd : keyHandler,
@@ -39,6 +47,7 @@ export class SuiTracker extends SuiMapper implements TrackerKeyHandler {
39
47
  idleTimer: number = Date.now();
40
48
  musicCursorGlyph: SVGSVGElement | null = null;
41
49
  deferPlayAdvance: boolean = false;
50
+ delegate: TrackerDelegate | null = null;
42
51
  static get strokes(): Record<string, StrokeInfo> {
43
52
  return {
44
53
  suggestion: {
@@ -72,7 +81,6 @@ export class SuiTracker extends SuiMapper implements TrackerKeyHandler {
72
81
  strokeDasharray: 0,
73
82
  opacity: 1.0
74
83
  }
75
-
76
84
  };
77
85
  }
78
86
  constructor(renderer: SuiRendererBase, scroller: SuiScroller) {
@@ -443,21 +451,48 @@ export class SuiTracker extends SuiMapper implements TrackerKeyHandler {
443
451
  this.idleTimer = Date.now();
444
452
  const nselector = JSON.parse(JSON.stringify(this.selections[0].selector));
445
453
  nselector.staff = this.score.incrementActiveStaff(offset);
446
-
454
+
447
455
  this.selections = [this._getClosestTick(nselector)];
448
456
  this.deferHighlight();
449
457
  this._createLocalModifiersList();
450
458
  }
451
- removePitchSelection() {
452
- if (this.outlines['pitchSelection']) {
453
- if (this.outlines['pitchSelection'].element) {
454
- this.outlines['pitchSelection'].element.remove();
455
- }
456
- delete this.outlines['pitchSelection'];
459
+
460
+
461
+ /**
462
+ * Set a specific pitch index (called by caret for mouse clicks)
463
+ * This updates the visual pitch selection without modifying data
464
+ */
465
+ setPitchIndex(index: number): void {
466
+ if (!this.selections.length) {
467
+ return;
468
+ }
469
+
470
+ const note = this.selections[0].note as SmoNote;
471
+ if (!note || index < 0 || index >= note.pitches.length) {
472
+ return;
457
473
  }
474
+
475
+ // Update the selected pitch index
476
+ this.pitchIndex = index;
477
+ this.selections[0].selector.pitches = [index];
478
+
479
+ // Notify via callback
480
+ this.delegate?.onPitchIndexChanged(index);
481
+ }
482
+
483
+ getPitchIndex(): number {
484
+ return this.pitchIndex;
485
+ }
486
+
487
+ clearPitchIndex(): void {
488
+ this.pitchIndex = -1;
489
+ if (this.selections.length) {
490
+ this.selections[0].selector.pitches = [];
491
+ }
492
+ // Notify via callback
493
+ this.delegate?.onPitchIndexChanged(-1);
458
494
  }
459
495
 
460
- // ### _moveSelectionPitch
461
496
  // Suggest a specific pitch in a chord, so we can transpose just the one note vs. the whole chord.
462
497
  _moveSelectionPitch(index: number) {
463
498
  this.idleTimer = Date.now();
@@ -468,13 +503,16 @@ export class SuiTracker extends SuiMapper implements TrackerKeyHandler {
468
503
  const note = sel.note as SmoNote;
469
504
  if (note.pitches.length < 2) {
470
505
  this.pitchIndex = -1;
471
- this.removePitchSelection();
506
+ // Emit event for pitch index reset
507
+ this.delegate?.onPitchIndexChanged(-1);
472
508
  return;
473
509
  }
474
510
  this.pitchIndex = (this.pitchIndex + index) % note.pitches.length;
475
511
  sel.selector.pitches = [];
476
512
  sel.selector.pitches.push(this.pitchIndex);
477
- this._highlightPitchSelection(note, this.pitchIndex);
513
+
514
+ // Notify via callback
515
+ this.delegate?.onPitchIndexChanged(this.pitchIndex);
478
516
  }
479
517
  moveSelectionPitchUp() {
480
518
  this._moveSelectionPitch(1);
@@ -617,7 +655,7 @@ export class SuiTracker extends SuiMapper implements TrackerKeyHandler {
617
655
  this.highlightQueue.selectionCount = this.selections.length;
618
656
  this.deferHighlight();
619
657
  }
620
- selectSuggestion(ev: KeyEvent) {
658
+ selectSuggestion(ev: KeyEvent, deferHighlightSelection: boolean = true) {
621
659
  if (!this.suggestion || !this.suggestion.measure || this.score === null) {
622
660
  return;
623
661
  }
@@ -648,7 +686,9 @@ export class SuiTracker extends SuiMapper implements TrackerKeyHandler {
648
686
  if (ev.ctrlKey) {
649
687
  this._addSelection(this.suggestion);
650
688
  this._createLocalModifiersList();
651
- this.deferHighlight();
689
+ if (deferHighlightSelection) {
690
+ this.deferHighlight();
691
+ }
652
692
  return;
653
693
  }
654
694
  if (this.autoPlay) {
@@ -690,7 +730,9 @@ export class SuiTracker extends SuiMapper implements TrackerKeyHandler {
690
730
  }
691
731
  }
692
732
  this.score.setActiveStaff(this.selections[0].selector.staff);
693
- this.deferHighlight();
733
+ if (deferHighlightSelection) {
734
+ this.deferHighlight();
735
+ }
694
736
  this._createLocalModifiersList();
695
737
  }
696
738
  _setModifierAsSuggestion(artifact: ModifierTab): void {
@@ -698,6 +740,9 @@ export class SuiTracker extends SuiMapper implements TrackerKeyHandler {
698
740
  return;
699
741
  }
700
742
  this.modifierSuggestion = artifact;
743
+ if (artifact.modifier instanceof SmoGraceNote) {
744
+ return;
745
+ }
701
746
  this._drawRect(artifact.box, 'suggestion');
702
747
  }
703
748
 
@@ -711,14 +756,27 @@ export class SuiTracker extends SuiMapper implements TrackerKeyHandler {
711
756
  break;
712
757
  }
713
758
  }
714
- if (sameSel || !artifact.box) {
759
+ if (!artifact.box) {
715
760
  return;
716
761
  }
762
+
717
763
  this.modifierSuggestion = null;
718
764
 
719
765
  this.suggestion = artifact;
766
+ // console.log(artifact.box);
767
+ // artifact.box.height = 60;
768
+ if (this.suggestion.note) {
769
+ return;
770
+ }
720
771
  this._drawRect(artifact.box, 'suggestion');
772
+ // if (sameSel) {
773
+ //todo; provide argument to the caret so it knows we are operation on a selected element
774
+ // }
775
+ // const caret = this.getNoteEntryCaret();
776
+ // caret.setSelection(this.suggestion);
777
+ // caret.render();
721
778
  }
779
+
722
780
  _highlightModifier() {
723
781
  let box: SvgBox | null = null;
724
782
  if (!this.modifierSelections.length) {
@@ -726,6 +784,9 @@ export class SuiTracker extends SuiMapper implements TrackerKeyHandler {
726
784
  }
727
785
  this.removeModifierSelectionBox();
728
786
  this.modifierSelections.forEach((artifact) => {
787
+ if (artifact.modifier instanceof SmoGraceNote) {
788
+ return;
789
+ }
729
790
  if (box === null) {
730
791
  box = artifact.modifier.logicalBox ?? null;
731
792
  } else {
@@ -738,19 +799,6 @@ export class SuiTracker extends SuiMapper implements TrackerKeyHandler {
738
799
  this._drawRect(box, 'staffModifier');
739
800
  }
740
801
 
741
- _highlightPitchSelection(note: SmoNote, index: number) {
742
- const noteDiv = $(this.renderElement).find('#' + note.renderId);
743
- const heads = noteDiv.find('.vf-notehead');
744
- if (!heads.length) {
745
- return;
746
- }
747
- const headEl = heads[index];
748
- const pageContext = this.renderer.pageMap.getRendererFromModifier(note);
749
- $(pageContext.svg).find('.vf-pitchSelection').remove();
750
- const box = pageContext.offsetBbox(headEl);
751
- this._drawRect(box, 'pitchSelection');
752
- }
753
-
754
802
  _highlightActiveVoice(selection: SmoSelection) {
755
803
  let i = 0;
756
804
  const selector = selection.selector;
@@ -775,32 +823,27 @@ export class SuiTracker extends SuiMapper implements TrackerKeyHandler {
775
823
  let prevSel: SmoSelection | null = null;
776
824
  let curBox: SvgBox = SvgBox.default;
777
825
  this.idleTimer = Date.now();
778
- const grace = this.getSelectedGraceNotes();
779
- // If this is not a note with grace notes, logically unselect the grace notes
780
- if (grace && grace.length && grace[0].selection && this.selections.length) {
781
- if (!SmoSelector.sameNote(grace[0].selection.selector, this.selections[0].selector)) {
782
- this.clearModifierSelections();
783
- } else {
784
- this._highlightModifier();
785
- return;
786
- }
787
- }
788
826
  // If there is a race condition with a change, avoid referencing null note
789
827
  if (!this.selections[0].note) {
790
828
  return;
791
829
  }
792
830
  const note = this.selections[0].note as SmoNote;
793
- if (this.pitchIndex >= 0 && this.selections.length === 1 &&
794
- this.pitchIndex < note.pitches.length) {
795
- this._highlightPitchSelection(note, this.pitchIndex);
796
- this._highlightActiveVoice(this.selections[0]);
797
- return;
831
+ // Reset pitch index if not in a valid pitch selection scenario
832
+ if (this.pitchIndex < 0 || this.selections.length !== 1 || this.pitchIndex >= note.pitches.length) {
833
+ this.pitchIndex = -1;
798
834
  }
799
- this.removePitchSelection();
800
- this.pitchIndex = -1;
835
+
836
+ // Single selection - just update voice UI and notify
801
837
  if (this.selections.length === 1 && note.logicalBox) {
802
- this._drawRect(note.logicalBox, 'selection');
803
838
  this._highlightActiveVoice(this.selections[0]);
839
+ // Clear any existing multi-selection rectangles
840
+ this.drawSelectionRects([]);
841
+ // Get selected grace note if any
842
+ let graceNote: SmoGraceNote | null = null;
843
+ if (this.modifierSelections.length > 0 && this.modifierSelections[0].modifier instanceof SmoGraceNote) {
844
+ graceNote = this.modifierSelections[0].modifier;
845
+ }
846
+ this.delegate?.onSingleNoteHighlighted(this.selections[0], graceNote);
804
847
  return;
805
848
  }
806
849
  const sorted = this.selections.sort((a, b) => SmoSelector.gt(a.selector, b.selector) ? 1 : -1);
@@ -901,4 +944,7 @@ export class SuiTracker extends SuiMapper implements TrackerKeyHandler {
901
944
  }
902
945
  });
903
946
  }
947
+
948
+
949
+
904
950
  }
@@ -241,6 +241,7 @@ export class VxMeasure implements VxMeasureIf {
241
241
  vexNote.setStave(this.stave);
242
242
  }
243
243
  }
244
+ smoNote.vexNote = vexNote;
244
245
  const tiedOverPitches = tickIndex === 0 ? this.tiedOverPitches : [];
245
246
  const noteData: VexNoteModifierIf = {
246
247
  smoMeasure: this.smoMeasure,
@@ -693,7 +694,7 @@ export class VxMeasure implements VxMeasureIf {
693
694
 
694
695
  this.rendered = true;
695
696
  if (layoutDebug.mask & layoutDebug.values['adjust']) {
696
- SvgHelpers.debugBoxNoText(this.context.getContext().svg,
697
+ SvgHelpers.debugBox(this.context.getContext().svg,
697
698
  SvgHelpers.boxPoints(this.dbgLeftX,
698
699
  this.smoMeasure.svg.staffY, this.dbgWidth, 40), 'render-x-dbg', 0);
699
700
  }
@@ -243,6 +243,7 @@ export class VxNote {
243
243
  gr.addClass('grace-note'); // note: this doesn't work :(
244
244
 
245
245
  g.renderId = gr.getAttribute('id');
246
+ g.vexGraceNote = gr;
246
247
  group.push(gr);
247
248
  });
248
249
  const grace: any = new VF.GraceNoteGroup(group);
@@ -318,6 +318,23 @@ export class SmoMusic {
318
318
  line += SmoMusic.clefLedgerShift[clef]
319
319
  return line;
320
320
  }
321
+ static staffLineToPitch(clef: string, line: number): Pitch {
322
+ const adjustedLine = line - SmoMusic.clefLedgerShift[clef];
323
+ const scaledValue = adjustedLine * 2;
324
+ const octaveValue = scaledValue + 28;
325
+ const octave = Math.floor(octaveValue / 7);
326
+ const positionInOctave = ((octaveValue % 7) + 7) % 7;
327
+
328
+ const noteKey = (Object.keys(SmoMusic.noteValues).find((key) =>
329
+ SmoMusic.noteValues[key].root_index === positionInOctave && key.length === 1
330
+ )) as PitchLetter;
331
+
332
+ return {
333
+ letter: noteKey,
334
+ accidental: 'n',
335
+ octave
336
+ };
337
+ }
321
338
  /**
322
339
  * return above if the first pitch is above line 3, else below
323
340
  * @param note
@@ -16,6 +16,7 @@ import { SmoMusic } from './music';
16
16
  import { Ticks, Pitch, SmoAttrs, Transposable, PitchLetter, SvgBox, getId } from './common';
17
17
  import { FontInfo, vexCanonicalNotes } from '../../common/vex';
18
18
  import { SmoTupletParamsSer } from './tuplet';
19
+ import {Note} from "vexflow_smoosic";
19
20
 
20
21
  export interface PlayedNote {
21
22
  pitches: Pitch[],
@@ -360,12 +361,13 @@ export class SmoNote implements Transposable {
360
361
  renderId: string | null = null;
361
362
  keySignature: string = 'c';
362
363
  logicalBox: SvgBox | null = null;
364
+ vexNote: Note | null = null;
363
365
  // mixin for real-time audio playback
364
366
  audioData: SmoAudioData = {
365
367
  volume: [],
366
368
  playedNotes: []
367
369
  };
368
-
370
+
369
371
  isCue: boolean = false;
370
372
  hasTabNote: boolean = true;
371
373
  accidentalsRendered: string[] = [];// set by renderer if accidental is to display
@@ -11,6 +11,7 @@ import { SmoAttrs, Ticks, Pitch, getId, SmoObjectParams, Transposable, SvgBox, S
11
11
  import { smoSerialize } from '../../common/serializationHelpers';
12
12
  import { SmoMusic } from './music';
13
13
  import { defaultNoteScale, FontInfo, getChordSymbolGlyphFromCode } from '../../common/vex';
14
+ import {GraceNote} from "vexflow_smoosic";
14
15
 
15
16
  /**
16
17
  * A note modifier is anything that is mapped to the note, but not part of the
@@ -220,6 +221,7 @@ export class SmoGraceNote extends SmoNoteModifierBase implements Transposable {
220
221
  clef: string = 'treble';
221
222
  noteType: string = 'n';
222
223
  renderId: string | null = null;
224
+ vexGraceNote: GraceNote | null = null;//todo: this is added here to help with EntryCaret
223
225
  hasTabNote: boolean = false;
224
226
 
225
227
  tickCount() {
@@ -144,7 +144,6 @@ export interface SmoScorePreferencesParams {
144
144
  hideEmptyLines: boolean;
145
145
  transposingScore: boolean;
146
146
  showPartNames: boolean;
147
- horizontalDisplay?: boolean;
148
147
  }
149
148
  /**
150
149
  * Some default SMO behavior
@@ -167,7 +166,6 @@ export class SmoScorePreferences {
167
166
  autoScrollPlayback: boolean = true;
168
167
  transposingScore: boolean = false;
169
168
  showPartNames: boolean = false;
170
- horizontalDisplay: boolean;
171
169
  static get defaults(): SmoScorePreferencesParams {
172
170
  return {
173
171
  autoPlay: true,
@@ -197,7 +195,6 @@ export class SmoScorePreferences {
197
195
  this.showPartNames = false;
198
196
  }
199
197
  }
200
- this.horizontalDisplay = params.horizontalDisplay ?? false;
201
198
  }
202
199
  serialize(): SmoScorePreferencesParams {
203
200
  return {
@@ -0,0 +1,22 @@
1
+ .musicContainer {
2
+ user-select: none;
3
+ -webkit-user-select: none;
4
+ -moz-user-select: none;
5
+ -ms-user-select: none;
6
+ }
7
+
8
+ .pitch-preview {
9
+ cursor: default;
10
+ pointer-events: none;
11
+ /*user-select: none;*/
12
+ /*-webkit-user-select: none;*/
13
+ /*-moz-user-select: none;*/
14
+ /*-ms-user-select: none;*/
15
+ }
16
+
17
+ /*.note-entry-caret {*/
18
+ /* user-select: none;*/
19
+ /* -webkit-user-select: none;*/
20
+ /* -moz-user-select: none;*/
21
+ /* -ms-user-select: none;*/
22
+ /*}*/
@@ -15,8 +15,7 @@ const props: Props = defineProps<Props>();
15
15
  const { domId, commitCb, cancelCb, getPreferences } = { ...props };
16
16
  const preferences = getPreferences();
17
17
 
18
- type booleanTypes = 'autoPlay' | 'autoAdvance' | 'showPiano' | 'hideEmptyLines' | 'autoScrollPlayback' | 'transposingScore'
19
- | 'showPartNames' | 'horizontalDisplay';
18
+ type booleanTypes = 'autoPlay' | 'autoAdvance' | 'showPiano' | 'hideEmptyLines' | 'autoScrollPlayback' | 'transposingScore' | 'showPartNames';
20
19
  type numberTypes = 'defaultDupleDuration' | 'defaultTripleDuration';
21
20
 
22
21
  const defaultDoubleDurations: SelectOption[] = [
@@ -98,15 +97,6 @@ const updateNumber = (type: numberTypes) => {
98
97
  <label class="form-check-label" :for="getId('partNames')">Show part names in Score</label>
99
98
  </div>
100
99
  </div>
101
- <div class="row">
102
- <div class="col col-1">
103
- <input class="form-check-input" type="checkbox" v-model="preferences.horizontalDisplay"
104
- :id="getId('horizontalDisplay')" @change="updateBool('horizontalDisplay')"></input>
105
- </div>
106
- <div class="col col-5">
107
- <label class="form-check-label" :for="getId('hideEmptyLines')">Horizontal Display</label>
108
- </div>
109
- </div>
110
100
  <div class="row align-items-baseline mt-3" :id="getId('arp-row')">
111
101
  <div class="col col-3 float-end pe-0">
112
102
  <span :for="getId('duration-select1')" class="form-label">Default Duration (even meter):</span>