opensheetmusicdisplay/opensheetmusicdisplay

View on GitHub
src/MusicalScore/Graphical/GraphicalContinuousDynamicExpression.ts

Summary

Maintainability
F
1 wk
Test Coverage
import { GraphicalLine } from "./GraphicalLine";
import { StaffLine } from "./StaffLine";
import { GraphicalMeasure } from "./GraphicalMeasure";
import { ContDynamicEnum, ContinuousDynamicExpression } from "../VoiceData/Expressions/ContinuousExpressions/ContinuousDynamicExpression";
import { PointF2D } from "../../Common/DataObjects/PointF2D";
import { AbstractGraphicalExpression } from "./AbstractGraphicalExpression";
import { PlacementEnum } from "../VoiceData/Expressions/AbstractExpression";
import { SkyBottomLineCalculator } from "./SkyBottomLineCalculator";
import { ISqueezable } from "./ISqueezable";
import log from "loglevel";
import { SourceMeasure } from "../VoiceData/SourceMeasure";

/**
 * This class prepares the graphical elements for a continuous expression. It calculates the wedges and
 * wrappings if they are split over system breaks.
 */
export class GraphicalContinuousDynamicExpression extends AbstractGraphicalExpression implements ISqueezable {
    /** True if expression is split over system borders */
    private isSplittedPart: boolean;
    /** True if this expression should not be removed if re-rendered */
    private notToBeRemoved: boolean;
    /** Holds the line objects that can be drawn via implementation */
    private lines: GraphicalLine[] = [];
    private startMeasure: GraphicalMeasure;
    private endMeasure: GraphicalMeasure;
    //public StartIsEnd: boolean;
    public IsSoftAccent: boolean;

    /**
     * Create a new instance of the GraphicalContinuousDynamicExpression
     * @param continuousDynamic The continuous dynamic instruction read via ExpressionReader
     * @param staffLine The staffline where the expression is attached
     */
    constructor(continuousDynamic: ContinuousDynamicExpression, staffLine: StaffLine, measure: SourceMeasure) {
        super(staffLine, continuousDynamic, measure);

        this.isSplittedPart = false;
        this.notToBeRemoved = false;
    }

    //#region Getter / Setter

    /** The graphical measure where the parent continuous dynamic expression starts */
    public get StartMeasure(): GraphicalMeasure { return this.startMeasure; }
    public set StartMeasure(value: GraphicalMeasure) { this.startMeasure = value; }
    /** The graphical measure where the parent continuous dynamic expression ends */
    public get EndMeasure(): GraphicalMeasure { return this.endMeasure; }
    public set EndMeasure(value: GraphicalMeasure) { this.endMeasure = value; }
    /** The staff lin where the graphical dynamic expressions ends */
    public get EndStaffLine(): StaffLine { return this.endMeasure ? this.endMeasure.ParentStaffLine : undefined; }
    /**  Is true if this continuous expression is a wedge, that reaches over a system border and needs to be split into two. */
    public get IsSplittedPart(): boolean { return this.isSplittedPart; }
    public set IsSplittedPart(value: boolean) { this.isSplittedPart = value; }
    /**  Is true if the dynamic is not a symbol but a text instruction. E.g. "decrescendo" */
    public get IsVerbal(): boolean { return this.ContinuousDynamic.Label && this.ContinuousDynamic.Label.length > 0; }
    /** True if this expression should not be removed if re-rendered */
    public get NotToBeRemoved(): boolean { return this.notToBeRemoved; }
    public set NotToBeRemoved(value: boolean) { this.notToBeRemoved = value; }
    /** Holds the line objects that can be drawn via implementation */
    public get Lines(): GraphicalLine[] { return this.lines; }

    public get ContinuousDynamic(): ContinuousDynamicExpression { return this.SourceExpression as ContinuousDynamicExpression; }
    //#endregion

    //#region Public methods

    public updateSkyBottomLine(): void {
        // update Sky-BottomLine
        const skyBottomLineCalculator: SkyBottomLineCalculator = this.parentStaffLine.SkyBottomLineCalculator;
        const left: number = this.IsVerbal ? this.label.PositionAndShape.RelativePosition.x + this.label.PositionAndShape.BorderMarginLeft : 0;
        const right: number = this.IsVerbal ? this.label.PositionAndShape.RelativePosition.x + this.label.PositionAndShape.BorderMarginRight : 0;
        if (!this.IsSoftAccent && !this.IsVerbal && this.lines.length < 2) {
            log.warn("Not enough lines for SkyBottomLine calculation");
        }
        if (!this.IsVerbal) {
            if (this.ContinuousDynamic.DynamicType !== ContDynamicEnum.crescendo &&
                this.ContinuousDynamic.DynamicType !== ContDynamicEnum.diminuendo) {
                // for now there is only crescendo or decrescendo anyways, but this will catch errors when we add new types in the future
                log.warn("GraphicalContinuousDynamicExpression.updateSkyBottomLine(): " +
                    "unhandled continuous dynamic type. start measure: " + this.startMeasure?.MeasureNumber);
            }
        }
        switch (this.Placement) {
            case PlacementEnum.Above:
                if (this.IsSoftAccent) {
                    skyBottomLineCalculator.updateSkyLineWithWedge(this.lines[0].Start, this.lines[0].End);
                    skyBottomLineCalculator.updateSkyLineWithWedge(this.lines[2].End, this.lines[2].Start);
                    skyBottomLineCalculator.updateSkyLineWithLine(this.lines[0].End, this.lines[2].End, this.lines[0].End.y);
                } else if (!this.IsVerbal) {
                    if (this.ContinuousDynamic.DynamicType === ContDynamicEnum.crescendo) {
                        skyBottomLineCalculator.updateSkyLineWithWedge(this.lines[0].Start, this.lines[0].End);
                    } else if (this.ContinuousDynamic.DynamicType === ContDynamicEnum.diminuendo) {
                        skyBottomLineCalculator.updateSkyLineWithWedge(this.lines[0].End, this.lines[0].Start);
                    } // else covered with the log.warn above
                } else {
                    const yValue: number = this.label.PositionAndShape.BorderMarginTop + this.label.PositionAndShape.RelativePosition.y;
                    skyBottomLineCalculator.updateSkyLineInRange(left, right, yValue);
                }
                break;
            case PlacementEnum.Below:
                if (!this.IsVerbal) {
                    // console.log(`id: ${this.parentStaffLine.ParentStaff.Id}`);
                    if (this.ContinuousDynamic.DynamicType === ContDynamicEnum.crescendo) {
                        skyBottomLineCalculator.updateBottomLineWithWedge(this.lines[1].Start, this.lines[1].End);
                    } else if (this.ContinuousDynamic.DynamicType === ContDynamicEnum.diminuendo) {
                        skyBottomLineCalculator.updateBottomLineWithWedge(this.lines[1].End, this.lines[1].Start);
                    } // else covered with the log.warn above
                } else {
                    const yValue: number = this.label.PositionAndShape.BorderMarginBottom + this.label.PositionAndShape.RelativePosition.y;
                    skyBottomLineCalculator.updateBottomLineInRange(left, right, yValue);
                }
                break;
            default:
                log.error("Placement for GraphicalContinuousDynamicExpression is unknown");
        }
    }

    /**
     * Calculate crescendo lines for (full).
     * @param startX left most starting point
     * @param endX right mist ending point
     * @param y y placement
     * @param wedgeOpeningLength length of the opening
     * @param wedgeLineWidth line width of the wedge
     */
    public createCrescendoLines(startX: number, endX: number, y: number,
                                wedgeOpeningLength: number = this.rules.WedgeOpeningLength, wedgeLineWidth: number = this.rules.WedgeLineWidth): void {
        const lineStart: PointF2D = new PointF2D(startX, y);
        const upperLineEnd: PointF2D = new PointF2D(endX, y - wedgeOpeningLength / 2);
        const lowerLineEnd: PointF2D = new PointF2D(endX, y + wedgeOpeningLength / 2);
        this.addWedgeLines(lineStart, upperLineEnd, lowerLineEnd, wedgeLineWidth);
    }

    /**
     * Calculate crescendo lines for system break (first part).
     * @param startX left most starting point
     * @param endX right mist ending point
     * @param y y placement
     * @param wedgeMeasureEndOpeningLength length of opening at measure end
     * @param wedgeOpeningLength length of the opening
     * @param wedgeLineWidth line width of the wedge
     */
    public createFirstHalfCrescendoLines(startX: number, endX: number, y: number,
                                         wedgeMeasureEndOpeningLength: number = this.rules.WedgeMeasureEndOpeningLength,
                                         wedgeLineWidth: number = this.rules.WedgeLineWidth): void {
        const lineStart: PointF2D = new PointF2D(startX, y);
        const upperLineEnd: PointF2D = new PointF2D(endX, y - wedgeMeasureEndOpeningLength / 2);
        const lowerLineEnd: PointF2D = new PointF2D(endX, y + wedgeMeasureEndOpeningLength / 2);
        this.addWedgeLines(lineStart, upperLineEnd, lowerLineEnd, wedgeLineWidth);
    }


    /**
     * Calculate crescendo lines for system break (second part).
     * @param startX left most starting point
     * @param endX right mist ending point
     * @param y y placement
     * @param wedgeMeasureBeginOpeningLength length of opening at measure start
     * @param wedgeOpeningLength length of the opening
     * @param wedgeLineWidth line width of the wedge
     */
    public createSecondHalfCrescendoLines(startX: number, endX: number, y: number,
                                          wedgeMeasureBeginOpeningLength: number = this.rules.WedgeMeasureBeginOpeningLength,
                                          wedgeOpeningLength: number = this.rules.WedgeOpeningLength,
                                          wedgeLineWidth: number = this.rules.WedgeLineWidth): void {
        const upperLineStart: PointF2D = new PointF2D(startX, y - wedgeMeasureBeginOpeningLength / 2);
        const lowerLineStart: PointF2D = new PointF2D(startX, y + wedgeMeasureBeginOpeningLength / 2);
        const upperLineEnd: PointF2D = new PointF2D(endX, y - wedgeOpeningLength / 2);
        const lowerLineEnd: PointF2D = new PointF2D(endX, y + wedgeOpeningLength / 2);
        this.addDoubleLines(upperLineStart, upperLineEnd, lowerLineStart, lowerLineEnd, wedgeLineWidth);
    }

    /**
     * This method recalculates the Crescendo Lines (for all cases).
     * @param startX left most starting point
     * @param endX right most ending point
     * @param y y placement
     */
    public recalculateCrescendoLines(startX: number, endX: number, y: number): void {
        const isSecondHalfSplit: boolean = Math.abs(this.lines[0].Start.y - this.lines[1].Start.y) > 0.0001;
        this.lines.clear();

        if (isSecondHalfSplit) {
            this.createSecondHalfCrescendoLines(startX, endX, y);
        } else if (this.isSplittedPart) {
            this.createFirstHalfCrescendoLines(startX, endX, y);
        } else {
            this.createCrescendoLines(startX, endX, y);
        }
    }

    /**
     * Calculate diminuendo lines for system break (full).
     * @param startX left most starting point
     * @param endX right mist ending point
     * @param y y placement
     * @param wedgeOpeningLength length of the opening
     * @param wedgeLineWidth line width of the wedge
     */
    public createDiminuendoLines(startX: number, endX: number, y: number,
                                 wedgeOpeningLength: number = this.rules.WedgeOpeningLength, wedgeLineWidth: number = this.rules.WedgeLineWidth): void {
        const upperWedgeStart: PointF2D = new PointF2D(startX, y - wedgeOpeningLength / 2);
        const lowerWedgeStart: PointF2D = new PointF2D(startX, y + wedgeOpeningLength / 2);
        const wedgeEnd: PointF2D = new PointF2D(endX, y);
        this.addWedgeLines(wedgeEnd, upperWedgeStart, lowerWedgeStart, wedgeLineWidth);
    }

    /**
     * Calculate diminuendo lines for system break (first part).
     * @param startX left most starting point
     * @param endX right mist ending point
     * @param y y placement
     * @param wedgeOpeningLength length of the opening
     * @param wedgeMeasureEndOpeningLength length of opening at measure end
     * @param wedgeLineWidth line width of the wedge
     */
    public createFirstHalfDiminuendoLines(startX: number, endX: number, y: number,
                                          wedgeOpeningLength: number = this.rules.WedgeOpeningLength,
                                          wedgeMeasureEndOpeningLength: number = this.rules.WedgeMeasureEndOpeningLength,
                                          wedgeLineWidth: number = this.rules.WedgeLineWidth): void {
        const upperLineStart: PointF2D = new PointF2D(startX, y - wedgeOpeningLength / 2);
        const lowerLineStart: PointF2D = new PointF2D(startX, y + wedgeOpeningLength / 2);
        const upperLineEnd: PointF2D = new PointF2D(endX, y - wedgeMeasureEndOpeningLength / 2);
        const lowerLineEnd: PointF2D = new PointF2D(endX, y + wedgeMeasureEndOpeningLength / 2);
        this.addDoubleLines(upperLineStart, upperLineEnd, lowerLineStart, lowerLineEnd, wedgeLineWidth);
    }

    /**
     * Calculate diminuendo lines for system break (second part).
     * @param startX left most starting point
     * @param endX right mist ending point
     * @param y y placement
     * @param wedgeMeasureBeginOpeningLength length of opening at measure start
     * @param wedgeLineWidth line width of the wedge
     */
    public createSecondHalfDiminuendoLines(startX: number, endX: number, y: number,
                                           wedgeMeasureBeginOpeningLength: number = this.rules.WedgeMeasureBeginOpeningLength,
                                           wedgeLineWidth: number = this.rules.WedgeLineWidth): void {
        const upperLineStart: PointF2D = new PointF2D(startX, y - wedgeMeasureBeginOpeningLength / 2);
        const lowerLineStart: PointF2D = new PointF2D(startX, y + wedgeMeasureBeginOpeningLength / 2);
        const lineEnd: PointF2D = new PointF2D(endX, y);
        this.addWedgeLines(lineEnd, upperLineStart, lowerLineStart, wedgeLineWidth);
    }

    /**
     * This method recalculates the diminuendo lines (for all cases).
     * @param startX left most starting point
     * @param endX right most ending point
     * @param y y placement
     */
    public recalculateDiminuendoLines(startX: number, endX: number, yPosition: number): void {
        const isFirstHalfSplit: boolean = Math.abs(this.lines[0].End.y - this.lines[1].End.y) > 0.0001;
        this.lines.clear();
        if (isFirstHalfSplit) {
            this.createFirstHalfDiminuendoLines(startX, endX, yPosition);
        } else if (this.isSplittedPart) {
            this.createSecondHalfDiminuendoLines(startX, endX, yPosition);
        } else {
            this.createDiminuendoLines(startX, endX, yPosition);
        }
    }

    /** Wrapper for createFirstHalfCrescendoLines and createFirstHalfDiminuendoLines.
     * Checks whether `this` is crescendo or diminuendo, helps avoid code duplication.
     */
    public createFirstHalfLines(startX: number, endX: number, y: number,
        wedgeOpeningLength: number = this.rules.WedgeOpeningLength,
        wedgeMeasureEndOpeningLength: number = this.rules.WedgeMeasureEndOpeningLength,
        wedgeLineWidth: number = this.rules.WedgeLineWidth
    ): void {
        if (this.ContinuousDynamic.DynamicType === ContDynamicEnum.crescendo) {
            this.createFirstHalfCrescendoLines(startX, endX, y,
                wedgeMeasureEndOpeningLength, wedgeLineWidth);
        } else if (this.ContinuousDynamic.DynamicType === ContDynamicEnum.diminuendo) {
            this.createFirstHalfDiminuendoLines(startX, endX, y,
                wedgeOpeningLength, wedgeMeasureEndOpeningLength, wedgeLineWidth);
        }
    }

    /** Wrapper for createSecondHalfCrescendoLines and createSecondHalfDiminuendoLines, see createFirstHalfLines. */
    public createSecondHalfLines(startX: number, endX: number, y: number,
        wedgeMeasureBeginOpeningLength: number = this.rules.WedgeMeasureBeginOpeningLength,
        wedgeOpeningLength: number = this.rules.WedgeOpeningLength,
        wedgeLineWidth: number = this.rules.WedgeLineWidth
    ): void {
        if (this.ContinuousDynamic.DynamicType === ContDynamicEnum.crescendo) {
            this.createSecondHalfCrescendoLines(startX, endX, y,
                wedgeMeasureBeginOpeningLength, wedgeOpeningLength, wedgeLineWidth);
        } else if (this.ContinuousDynamic.DynamicType === ContDynamicEnum.diminuendo) {
            this.createSecondHalfDiminuendoLines(startX, endX, y,
                wedgeMeasureBeginOpeningLength, wedgeLineWidth);
        }
    }

    /** Wrapper for createCrescendoLines and createDiminuendoLines, see createFirstHalfLines. */
    public createLines(startX: number, endX: number, y: number,
        wedgeOpeningLength: number = this.rules.WedgeOpeningLength, wedgeLineWidth: number = this.rules.WedgeLineWidth
    ): void {
        if (this.ContinuousDynamic.DynamicType === ContDynamicEnum.crescendo) {
            this.createCrescendoLines(startX, endX, y,
                wedgeOpeningLength, wedgeLineWidth);
        } else if (this.ContinuousDynamic.DynamicType === ContDynamicEnum.diminuendo) {
            this.createDiminuendoLines(startX, endX, y,
                wedgeOpeningLength, wedgeLineWidth);
        }
    }

    /**
     * Calculate the BoundingBox (as a box around the Wedge).
     */
    public calcPsi(): void {
        if (this.IsVerbal) {
            this.PositionAndShape.calculateBoundingBox();
            return;
        }
        this.PositionAndShape.RelativePosition = this.lines[0].Start;
        this.PositionAndShape.BorderMarginTop = this.lines[0].End.y - this.lines[0].Start.y;
        this.PositionAndShape.BorderMarginBottom = this.lines[1].End.y - this.lines[1].Start.y;
        this.PositionAndShape.Center.y = (this.PositionAndShape.BorderMarginTop + this.PositionAndShape.BorderMarginBottom) / 2;
        // TODO is the center position correct? it wasn't set before, important for AlignmentManager.alignDynamicExpressions()
        // console.log(`relative y, center y: ${this.PositionAndShape.RelativePosition.y},${this.PositionAndShape.Center.y})`);

        if (this.IsSoftAccent) {
            this.PositionAndShape.BorderMarginLeft = 0;
            this.PositionAndShape.BorderMarginRight = this.lines[3].Start.x - this.lines[0].Start.x;
        } else if (this.ContinuousDynamic.DynamicType === ContDynamicEnum.crescendo) {
            this.PositionAndShape.BorderMarginLeft = 0;
            this.PositionAndShape.BorderMarginRight = this.lines[0].End.x - this.lines[0].Start.x;
        } else {
            this.PositionAndShape.BorderMarginLeft = this.lines[0].End.x - this.lines[0].Start.x;
            this.PositionAndShape.BorderMarginRight = 0;
        }
    }

    /**
     * Clear Lines
     */
    public cleanUp(): void {
        this.lines.clear();
    }

    /**
     * Shift wedge in y position
     * @param shift Number to shift
     */
    public shiftYPosition(shift: number): void {
        if (this.IsVerbal) {
            this.PositionAndShape.RelativePosition.y += shift;
            this.PositionAndShape.calculateBoundingBox();
        } else {
            this.lines[0].Start.y += shift;
            this.lines[0].End.y += shift;
            this.lines[1].End.y += shift;
        }
    }

    public squeeze(value: number): void {
        // Verbal expressions are not squeezable and squeezing below the width is also not possible
        if (this.IsVerbal) {
            return;
        }
        const width: number = Math.abs(this.lines[0].End.x - this.lines[0].Start.x);
        if (width < Math.abs(value)) {
            return;
        }
        if (this.ContinuousDynamic.DynamicType === ContDynamicEnum.crescendo) {
            if (value > 0) {
                this.lines[0].Start.x += value;
            } else {
                this.lines[0].End.x += value;
                this.lines[1].End.x += value;
            }
        } else {
            if (value < 0) {
                this.lines[0].Start.x += value;
            } else {
                this.lines[0].End.x += value;
                this.lines[1].End.x += value;
            }
        }
        this.calcPsi();
    }

    //#endregion

    //#region Private methods

    /**
     * Create lines from points and add them to the memory
     * @param wedgePoint start of the expression
     * @param upperWedgeEnd end of the upper line
     * @param lowerWedgeEnd end of lower line
     * @param wedgeLineWidth line width
     */
    private addWedgeLines(wedgePoint: PointF2D, upperWedgeEnd: PointF2D, lowerWedgeEnd: PointF2D, wedgeLineWidth: number): void {
        const upperLine: GraphicalLine = new GraphicalLine(wedgePoint, upperWedgeEnd, wedgeLineWidth);
        const lowerLine: GraphicalLine = new GraphicalLine(wedgePoint, lowerWedgeEnd, wedgeLineWidth);
        if (this.rules.DefaultColorMusic) {
            upperLine.colorHex = this.rules.DefaultColorMusic;
            lowerLine.colorHex = this.rules.DefaultColorMusic;
        }

        this.lines.push(upperLine);
        this.lines.push(lowerLine);
    }

    /**
     * Create top and bottom lines for continuing wedges
     * @param upperLineStart start of the upper line
     * @param upperLineEnd end of the upper line
     * @param lowerLineStart start of the lower line
     * @param lowerLineEnd end of lower line
     * @param wedgeLineWidth line width
     */
    private addDoubleLines(upperLineStart: PointF2D, upperLineEnd: PointF2D, lowerLineStart: PointF2D, lowerLineEnd: PointF2D, wedgeLineWidth: number): void {
        const upperLine: GraphicalLine = new GraphicalLine(upperLineStart, upperLineEnd, wedgeLineWidth);
        const lowerLine: GraphicalLine = new GraphicalLine(lowerLineStart, lowerLineEnd, wedgeLineWidth);
        if (this.rules.DefaultColorMusic) {
            upperLine.colorHex = this.rules.DefaultColorMusic;
            lowerLine.colorHex = this.rules.DefaultColorMusic;
        }

        this.lines.push(upperLine);
        this.lines.push(lowerLine);
    }

    //#endregion
}