|  | @@ -23,6 +23,11 @@ import {CollectionUtil} from "../../Util/CollectionUtil";
 | 
	
		
			
				|  |  |  import {SelectionStartSymbol} from "./SelectionStartSymbol";
 | 
	
		
			
				|  |  |  import {SelectionEndSymbol} from "./SelectionEndSymbol";
 | 
	
		
			
				|  |  |  import {OutlineAndFillStyleEnum} from "./DrawingEnums";
 | 
	
		
			
				|  |  | +import { MusicSheetDrawer } from "./MusicSheetDrawer";
 | 
	
		
			
				|  |  | +import { GraphicalVoiceEntry } from "./GraphicalVoiceEntry";
 | 
	
		
			
				|  |  | +import { GraphicalObject } from "./GraphicalObject";
 | 
	
		
			
				|  |  | +// import { VexFlowMusicSheetDrawer } from "./VexFlow/VexFlowMusicSheetDrawer";
 | 
	
		
			
				|  |  | +// import { SvgVexFlowBackend } from "./VexFlow/SvgVexFlowBackend"; // causes build problem with npm start
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  /**
 | 
	
		
			
				|  |  |   * The graphical counterpart of a [[MusicSheet]]
 | 
	
	
		
			
				|  | @@ -38,6 +43,7 @@ export class GraphicalMusicSheet {
 | 
	
		
			
				|  |  |      private musicSheet: MusicSheet;
 | 
	
		
			
				|  |  |      //private fontInfo: FontInfo = FontInfo.Info;
 | 
	
		
			
				|  |  |      private calculator: MusicSheetCalculator;
 | 
	
		
			
				|  |  | +    public drawer: MusicSheetDrawer;
 | 
	
		
			
				|  |  |      private musicPages: GraphicalMusicPage[] = [];
 | 
	
		
			
				|  |  |      /** measures (i,j) where i is the measure number and j the staff index (e.g. staff indices 0, 1 for two piano parts) */
 | 
	
		
			
				|  |  |      private measureList: GraphicalMeasure[][] = [];
 | 
	
	
		
			
				|  | @@ -495,61 +501,136 @@ export class GraphicalMusicSheet {
 | 
	
		
			
				|  |  |          return false;
 | 
	
		
			
				|  |  |      }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -    public GetNearestNote(clickPosition: PointF2D, maxClickDist: PointF2D): GraphicalNote {
 | 
	
		
			
				|  |  | -        const initialSearchArea: number = 10;
 | 
	
		
			
				|  |  | -        const foundNotes: GraphicalNote[] = [];
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -        // Prepare search area
 | 
	
		
			
				|  |  | -        const region: BoundingBox = new BoundingBox();
 | 
	
		
			
				|  |  | -        region.BorderLeft = clickPosition.x - initialSearchArea;
 | 
	
		
			
				|  |  | -        region.BorderTop = clickPosition.y - initialSearchArea;
 | 
	
		
			
				|  |  | -        region.BorderRight = clickPosition.x + initialSearchArea;
 | 
	
		
			
				|  |  | -        region.BorderBottom = clickPosition.y + initialSearchArea;
 | 
	
		
			
				|  |  | -        region.AbsolutePosition = new PointF2D(0, 0);
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -        // Search for StaffEntries in region
 | 
	
		
			
				|  |  | -        for (let idx: number = 0, len: number = this.MusicPages.length; idx < len; ++idx) {
 | 
	
		
			
				|  |  | -            const graphicalMusicPage: GraphicalMusicPage = this.MusicPages[idx];
 | 
	
		
			
				|  |  | -            const entries: GraphicalNote[] = graphicalMusicPage.PositionAndShape.getObjectsInRegion<GraphicalNote>(region);
 | 
	
		
			
				|  |  | -            //let entriesArr: GraphicalNote[] = __as__<GraphicalNote[]>(entries, GraphicalNote[]) ? ? entries;
 | 
	
		
			
				|  |  | -            if (!entries) {
 | 
	
		
			
				|  |  | -                continue;
 | 
	
		
			
				|  |  | -            } else {
 | 
	
		
			
				|  |  | -                for (let idx2: number = 0, len2: number = entries.length; idx2 < len2; ++idx2) {
 | 
	
		
			
				|  |  | -                    const note: GraphicalNote = entries[idx2];
 | 
	
		
			
				|  |  | -                    if (Math.abs(note.PositionAndShape.AbsolutePosition.x - clickPosition.x) < maxClickDist.x
 | 
	
		
			
				|  |  | -                        && Math.abs(note.PositionAndShape.AbsolutePosition.y - clickPosition.y) < maxClickDist.y) {
 | 
	
		
			
				|  |  | -                        foundNotes.push(note);
 | 
	
		
			
				|  |  | +    /**
 | 
	
		
			
				|  |  | +     * Generic method to find graphical objects on the sheet at a given location.
 | 
	
		
			
				|  |  | +     * @param clickPosition Position in units where we are searching on the sheet
 | 
	
		
			
				|  |  | +     * @param className String representation of the class we want to find. Must extend GraphicalObject
 | 
	
		
			
				|  |  | +     * @param startSearchArea The area in units around our point to look for our graphical object, default 5
 | 
	
		
			
				|  |  | +     * @param maxSearchArea The max area we want to search around our point
 | 
	
		
			
				|  |  | +     * @param searchAreaIncrement The amount we expand our search area for each iteration that we don't find an object of the given type
 | 
	
		
			
				|  |  | +     * @param shouldBeIncludedTest A callback that determines if the object should be included in our results- return false for no, true for yes
 | 
	
		
			
				|  |  | +     */
 | 
	
		
			
				|  |  | +    private GetNearestGraphicalObject<T extends GraphicalObject>(
 | 
	
		
			
				|  |  | +        clickPosition: PointF2D, className: string = GraphicalObject.name,
 | 
	
		
			
				|  |  | +        startSearchArea: number = 5, maxSearchArea: number = 20, searchAreaIncrement: number = 5,
 | 
	
		
			
				|  |  | +        shouldBeIncludedTest: (objectToTest: T) => boolean = undefined): T {
 | 
	
		
			
				|  |  | +        const foundEntries: T[] = [];
 | 
	
		
			
				|  |  | +        //Loop until we find some, or our search area is out of bounds
 | 
	
		
			
				|  |  | +        while (foundEntries.length === 0 && startSearchArea <= maxSearchArea) {
 | 
	
		
			
				|  |  | +            //Prepare search area
 | 
	
		
			
				|  |  | +            const region: BoundingBox = new BoundingBox(undefined);
 | 
	
		
			
				|  |  | +            region.BorderLeft = clickPosition.x - startSearchArea;
 | 
	
		
			
				|  |  | +            region.BorderTop = clickPosition.y - startSearchArea;
 | 
	
		
			
				|  |  | +            region.BorderRight = clickPosition.x + startSearchArea;
 | 
	
		
			
				|  |  | +            region.BorderBottom = clickPosition.y + startSearchArea;
 | 
	
		
			
				|  |  | +            region.AbsolutePosition = new PointF2D(clickPosition.x, clickPosition.y);
 | 
	
		
			
				|  |  | +            region.calculateAbsolutePosition();
 | 
	
		
			
				|  |  | +            //Loop through music pages
 | 
	
		
			
				|  |  | +            for (let idx: number = 0, len: number = this.MusicPages.length; idx < len; ++idx) {
 | 
	
		
			
				|  |  | +                const graphicalMusicPage: GraphicalMusicPage = this.MusicPages[idx];
 | 
	
		
			
				|  |  | +                const entries: T[] = graphicalMusicPage.PositionAndShape.getObjectsInRegion<T>(region, false, className);
 | 
	
		
			
				|  |  | +                //If we have no entries on this page, skip to next (if exists)
 | 
	
		
			
				|  |  | +                if (!entries || entries.length === 0) {
 | 
	
		
			
				|  |  | +                    continue;
 | 
	
		
			
				|  |  | +                } else {
 | 
	
		
			
				|  |  | +                    //Otherwise test all our entries if applicable, store on our found list
 | 
	
		
			
				|  |  | +                    for (let idx2: number = 0, len2: number = entries.length; idx2 < len2; ++idx2) {
 | 
	
		
			
				|  |  | +                        if (!shouldBeIncludedTest) {
 | 
	
		
			
				|  |  | +                            foundEntries.push(entries[idx2]);
 | 
	
		
			
				|  |  | +                        } else if (shouldBeIncludedTest(entries[idx2])) {
 | 
	
		
			
				|  |  | +                            foundEntries.push(entries[idx2]);
 | 
	
		
			
				|  |  | +                        }
 | 
	
		
			
				|  |  |                      }
 | 
	
		
			
				|  |  |                  }
 | 
	
		
			
				|  |  |              }
 | 
	
		
			
				|  |  | +            //Expand search area, we haven't found anything yet
 | 
	
		
			
				|  |  | +            startSearchArea += searchAreaIncrement;
 | 
	
		
			
				|  |  |          }
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  |          // Get closest entry
 | 
	
		
			
				|  |  | -        let closest: GraphicalNote = undefined;
 | 
	
		
			
				|  |  | -        for (let idx: number = 0, len: number = foundNotes.length; idx < len; ++idx) {
 | 
	
		
			
				|  |  | -            const note: GraphicalNote = foundNotes[idx];
 | 
	
		
			
				|  |  | +        let closest: T = undefined;
 | 
	
		
			
				|  |  | +        for (let idx: number = 0, len: number = foundEntries.length; idx < len; ++idx) {
 | 
	
		
			
				|  |  | +            const object: T = foundEntries[idx];
 | 
	
		
			
				|  |  |              if (closest === undefined) {
 | 
	
		
			
				|  |  | -                closest = note;
 | 
	
		
			
				|  |  | +                closest = object;
 | 
	
		
			
				|  |  |              } else {
 | 
	
		
			
				|  |  | -                if (!note.parentVoiceEntry.parentStaffEntry.relInMeasureTimestamp) {
 | 
	
		
			
				|  |  | -                    continue;
 | 
	
		
			
				|  |  | -                }
 | 
	
		
			
				|  |  | -                const deltaNew: number = this.CalculateDistance(note.PositionAndShape.AbsolutePosition, clickPosition);
 | 
	
		
			
				|  |  | +                const deltaNew: number = this.CalculateDistance(object.PositionAndShape.AbsolutePosition, clickPosition);
 | 
	
		
			
				|  |  |                  const deltaOld: number = this.CalculateDistance(closest.PositionAndShape.AbsolutePosition, clickPosition);
 | 
	
		
			
				|  |  |                  if (deltaNew < deltaOld) {
 | 
	
		
			
				|  |  | -                    closest = note;
 | 
	
		
			
				|  |  | +                    closest = object;
 | 
	
		
			
				|  |  |                  }
 | 
	
		
			
				|  |  |              }
 | 
	
		
			
				|  |  |          }
 | 
	
		
			
				|  |  |          if (closest) {
 | 
	
		
			
				|  |  |              return closest;
 | 
	
		
			
				|  |  |          }
 | 
	
		
			
				|  |  | -        // TODO No staff entry was found. Feedback?
 | 
	
		
			
				|  |  | -        // throw new ArgumentException("No staff entry found");
 | 
	
		
			
				|  |  |          return undefined;
 | 
	
		
			
				|  |  |      }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | +    public GetNearestVoiceEntry(clickPosition: PointF2D): GraphicalVoiceEntry {
 | 
	
		
			
				|  |  | +        return this.GetNearestGraphicalObject<GraphicalVoiceEntry>(clickPosition, GraphicalVoiceEntry.name, 5, 20, 5,
 | 
	
		
			
				|  |  | +                                                                   (object: GraphicalVoiceEntry) =>
 | 
	
		
			
				|  |  | +                                                                        object.parentStaffEntry.relInMeasureTimestamp !== undefined);
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    public GetNearestNote(clickPosition: PointF2D, maxClickDist: PointF2D): GraphicalNote {
 | 
	
		
			
				|  |  | +        const nearestVoiceEntry: GraphicalVoiceEntry = this.GetNearestVoiceEntry(clickPosition);
 | 
	
		
			
				|  |  | +        if (!nearestVoiceEntry) {
 | 
	
		
			
				|  |  | +            return undefined;
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  | +        let closestNote: GraphicalNote;
 | 
	
		
			
				|  |  | +        let closestDist: number = Number.MAX_SAFE_INTEGER;
 | 
	
		
			
				|  |  | +        // debug: show position in sheet. line starts from the click position, until clickposition.x + 2
 | 
	
		
			
				|  |  | +        // (this.drawer as any).DrawOverlayLine( // as VexFlowMusicSheetDrawer
 | 
	
		
			
				|  |  | +        //     clickPosition,
 | 
	
		
			
				|  |  | +        //     new PointF2D(clickPosition.x + 2, clickPosition.y),
 | 
	
		
			
				|  |  | +        //     this.MusicPages[0]);
 | 
	
		
			
				|  |  | +        for (const note of nearestVoiceEntry.notes) {
 | 
	
		
			
				|  |  | +            const posY: number = note.PositionAndShape.AbsolutePosition.y;
 | 
	
		
			
				|  |  | +            const distX: number = Math.abs(note.PositionAndShape.AbsolutePosition.x - clickPosition.x);
 | 
	
		
			
				|  |  | +            const distY: number = Math.abs(posY - clickPosition.y);
 | 
	
		
			
				|  |  | +            // console.log("note: " + note.sourceNote.Pitch.ToString());
 | 
	
		
			
				|  |  | +            if (distX + distY < closestDist) {
 | 
	
		
			
				|  |  | +                closestNote = note;
 | 
	
		
			
				|  |  | +                closestDist = distX + distY;
 | 
	
		
			
				|  |  | +            }
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  | +        return closestNote;
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    public domToSvg(point: PointF2D): PointF2D {
 | 
	
		
			
				|  |  | +        return this.domToSvgTransform(point, true);
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    public svgToDom(point: PointF2D): PointF2D {
 | 
	
		
			
				|  |  | +        return this.domToSvgTransform(point, false);
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    public svgToOsmd(point: PointF2D): PointF2D {
 | 
	
		
			
				|  |  | +        const pt: PointF2D = new PointF2D(point.x, point.y);
 | 
	
		
			
				|  |  | +        pt.x /= 10; // unitInPixels would need to be imported from VexFlowMusicSheetDrawer
 | 
	
		
			
				|  |  | +        pt.y /= 10;
 | 
	
		
			
				|  |  | +        return pt;
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    // TODO move to VexFlowMusicSheetDrawer? better fit for imports
 | 
	
		
			
				|  |  | +    private domToSvgTransform(point: PointF2D, inverse: boolean): PointF2D {
 | 
	
		
			
				|  |  | +        const svgBackend: any = (this.drawer as any).Backends[0]; // as SvgVexFlowBackend;
 | 
	
		
			
				|  |  | +        // TODO importing SvgVexFlowBackend here causes build problems. Importing VexFlowMusicSheetDrawer seems to be fine, but unnecessary.
 | 
	
		
			
				|  |  | +        // if (!(svgBackend instanceof SvgVexFlowBackend)) {
 | 
	
		
			
				|  |  | +        //     return undefined;
 | 
	
		
			
				|  |  | +        // }
 | 
	
		
			
				|  |  | +        const svg: SVGSVGElement = svgBackend.getSvgElement() as SVGSVGElement;
 | 
	
		
			
				|  |  | +        const pt: SVGPoint = svg.createSVGPoint();
 | 
	
		
			
				|  |  | +        pt.x = point.x;
 | 
	
		
			
				|  |  | +        pt.y = point.y;
 | 
	
		
			
				|  |  | +        let transformMatrix: DOMMatrix = svg.getScreenCTM();
 | 
	
		
			
				|  |  | +        if (inverse) {
 | 
	
		
			
				|  |  | +            transformMatrix = transformMatrix.inverse();
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  | +        const sp: SVGPoint = pt.matrixTransform(transformMatrix);
 | 
	
		
			
				|  |  | +        return new PointF2D(sp.x, sp.y);
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  |      public GetClickableLabel(clickPosition: PointF2D): GraphicalLabel {
 | 
	
		
			
				|  |  |          const initialSearchAreaX: number = 4;
 | 
	
		
			
				|  |  |          const initialSearchAreaY: number = 4;
 |