src/MusicalScore/Graphical/GraphicalContinuousDynamicExpression.ts
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
}