diff --git a/e2e/tests/basic-accidentals.musicxml b/e2e/tests/basic-accidentals.musicxml deleted file mode 100644 index 154a3ff..0000000 --- a/e2e/tests/basic-accidentals.musicxml +++ /dev/null @@ -1,104 +0,0 @@ - - - - - - - Music - - - - - - 256 - - -2 - major - - - - G - 2 - - - - - F - 4 - - 256 - quarter - - - - G - 4 - - 256 - quarter - - - - G - 1 - 4 - - 256 - quarter - sharp - - - - A - 4 - - 256 - quarter - - - - - - B - -1 - 4 - - 512 - half - - - - D - -1 - 5 - - 256 - quarter - flat - - - - D - -1 - 5 - - 256 - quarter - - - - - - D - 5 - - 1024 - whole - natural - - - - diff --git a/e2e/tests/basic-durations.musicxml b/e2e/tests/basic-durations.musicxml new file mode 100644 index 0000000..b1ce647 --- /dev/null +++ b/e2e/tests/basic-durations.musicxml @@ -0,0 +1,174 @@ + + + + + + MuseScore 4.1.1 + 2024-06-09 + + + + + + + + + + 6.99911 + 40 + + + 1596.77 + 1233.87 + + 85.7252 + 85.7252 + 85.7252 + 85.7252 + + + 85.7252 + 85.7252 + 85.7252 + 85.7252 + + + + + + + + Music + + + keyboard.piano.grand + + + + 1 + 1 + 78.7402 + 0 + + + + + + + + + 50.00 + -0.00 + + 170.00 + + + + 1 + + -2 + major + + + + G + 2 + + + + + B + -1 + 4 + + 1 + 1 + quarter + down + + + + B + -1 + 4 + + 1 + 1 + quarter + down + + + + B + -1 + 4 + + 1 + 1 + quarter + down + + + + B + -1 + 4 + + 1 + 1 + quarter + down + + + + + + B + -1 + 4 + + 2 + 1 + half + down + + + + B + -1 + 4 + + 1 + 1 + quarter + down + + + + B + -1 + 4 + + 1 + 1 + quarter + down + + + + + + B + -1 + 4 + + 4 + 1 + whole + + + + diff --git a/e2e/tests/default-score.spec.ts b/e2e/tests/default-score.spec.ts index 194ecc3..e4b5c27 100644 --- a/e2e/tests/default-score.spec.ts +++ b/e2e/tests/default-score.spec.ts @@ -29,7 +29,7 @@ test("renders 3 measures", async ({ page }) => { }) test("renders score from musicxml", async ({ page }) => { - const inputXmlString = fs.readFileSync(path.join(__dirname, "basic-accidentals.musicxml"), "utf8") + const inputXmlString = fs.readFileSync(path.join(__dirname, "basic-durations.musicxml"), "utf8") await page.evaluate((xml) => { window.scoreStorm.setScore(window.getScoreFormMusicXml(xml)) }, inputXmlString) diff --git a/e2e/tests/default-score.spec.ts-snapshots/renders-3-measures-1-chromium-canvas-darwin.png b/e2e/tests/default-score.spec.ts-snapshots/renders-3-measures-1-chromium-canvas-darwin.png index 9f5d630..c8d4c5d 100644 Binary files a/e2e/tests/default-score.spec.ts-snapshots/renders-3-measures-1-chromium-canvas-darwin.png and b/e2e/tests/default-score.spec.ts-snapshots/renders-3-measures-1-chromium-canvas-darwin.png differ diff --git a/e2e/tests/default-score.spec.ts-snapshots/renders-3-measures-1-webkit-canvas-darwin.png b/e2e/tests/default-score.spec.ts-snapshots/renders-3-measures-1-webkit-canvas-darwin.png index 5146412..ff369de 100644 Binary files a/e2e/tests/default-score.spec.ts-snapshots/renders-3-measures-1-webkit-canvas-darwin.png and b/e2e/tests/default-score.spec.ts-snapshots/renders-3-measures-1-webkit-canvas-darwin.png differ diff --git a/e2e/tests/default-score.spec.ts-snapshots/renders-default-score-1-chromium-canvas-darwin.png b/e2e/tests/default-score.spec.ts-snapshots/renders-default-score-1-chromium-canvas-darwin.png index 16ee853..8735985 100644 Binary files a/e2e/tests/default-score.spec.ts-snapshots/renders-default-score-1-chromium-canvas-darwin.png and b/e2e/tests/default-score.spec.ts-snapshots/renders-default-score-1-chromium-canvas-darwin.png differ diff --git a/e2e/tests/default-score.spec.ts-snapshots/renders-default-score-1-webkit-canvas-darwin.png b/e2e/tests/default-score.spec.ts-snapshots/renders-default-score-1-webkit-canvas-darwin.png index 3263f3e..3bad70d 100644 Binary files a/e2e/tests/default-score.spec.ts-snapshots/renders-default-score-1-webkit-canvas-darwin.png and b/e2e/tests/default-score.spec.ts-snapshots/renders-default-score-1-webkit-canvas-darwin.png differ diff --git a/e2e/tests/default-score.spec.ts-snapshots/renders-score-from-musicxml-1-chromium-canvas-darwin.png b/e2e/tests/default-score.spec.ts-snapshots/renders-score-from-musicxml-1-chromium-canvas-darwin.png index ea10fa2..6ba5d32 100644 Binary files a/e2e/tests/default-score.spec.ts-snapshots/renders-score-from-musicxml-1-chromium-canvas-darwin.png and b/e2e/tests/default-score.spec.ts-snapshots/renders-score-from-musicxml-1-chromium-canvas-darwin.png differ diff --git a/e2e/tests/default-score.spec.ts-snapshots/renders-score-from-musicxml-1-webkit-canvas-darwin.png b/e2e/tests/default-score.spec.ts-snapshots/renders-score-from-musicxml-1-webkit-canvas-darwin.png index a9c2afa..38ceab1 100644 Binary files a/e2e/tests/default-score.spec.ts-snapshots/renders-score-from-musicxml-1-webkit-canvas-darwin.png and b/e2e/tests/default-score.spec.ts-snapshots/renders-score-from-musicxml-1-webkit-canvas-darwin.png differ diff --git a/e2e/tests/features/accidentals.spec.ts b/e2e/tests/features/accidentals.spec.ts new file mode 100644 index 0000000..1e96471 --- /dev/null +++ b/e2e/tests/features/accidentals.spec.ts @@ -0,0 +1,28 @@ +import { expect } from "@playwright/test" +import fs from "fs" +import path from "path" +import { test } from "../parametrized-test" + +// todo: move to global setup +test.beforeEach(async ({ page, renderer }) => { + await page.goto("/") + await page.evaluate((renderer) => { + window.scoreStorm.setRenderer(renderer === "svg" ? window.svgRenderer : window.canvasRenderer) + }, renderer) +}) + +// todo: move to global setup +test.afterEach(async ({ page }) => { + await page.evaluate(() => { + window.scoreStorm.render() + }) + + await expect(page.locator("#ss-container")).toHaveScreenshot() +}) + +test("renders accidentals", async ({ page }) => { + const inputXmlString = fs.readFileSync(path.join(__dirname, "basic-accidentals.musicxml"), "utf8") + await page.evaluate((xml) => { + window.scoreStorm.setScore(window.getScoreFormMusicXml(xml)) + }, inputXmlString) +}) diff --git a/e2e/tests/features/accidentals.spec.ts-snapshots/renders-accidentals-1-features-darwin.png b/e2e/tests/features/accidentals.spec.ts-snapshots/renders-accidentals-1-features-darwin.png new file mode 100644 index 0000000..e398e84 Binary files /dev/null and b/e2e/tests/features/accidentals.spec.ts-snapshots/renders-accidentals-1-features-darwin.png differ diff --git a/e2e/tests/features/basic-accidentals.musicxml b/e2e/tests/features/basic-accidentals.musicxml new file mode 100644 index 0000000..2e644dd --- /dev/null +++ b/e2e/tests/features/basic-accidentals.musicxml @@ -0,0 +1,180 @@ + + + + + + MuseScore 4.1.1 + 2024-06-09 + + + + + + + + + + 6.99911 + 40 + + + 1596.77 + 1233.87 + + 85.7252 + 85.7252 + 85.7252 + 85.7252 + + + 85.7252 + 85.7252 + 85.7252 + 85.7252 + + + + + + + + Music + + + keyboard.piano.grand + + + + 1 + 1 + 78.7402 + 0 + + + + + + + + + 50.00 + -0.00 + + 170.00 + + + + 1 + + 0 + + + + G + 2 + + + + + B + 1 + 4 + + 1 + 1 + quarter + sharp + down + + + + B + -1 + 4 + + 1 + 1 + quarter + flat + down + + + + B + 4 + + 1 + 1 + quarter + natural + down + + + + B + -1 + 4 + + 1 + 1 + quarter + flat + down + + + + + + B + -2 + 4 + + 1 + 1 + quarter + flat-flat + down + + + + B + -2 + 4 + + 1 + 1 + quarter + down + + + + B + 4 + + 1 + 1 + quarter + natural + down + + + + B + 2 + 4 + + 1 + 1 + quarter + double-sharp + down + + + light-heavy + + + + diff --git a/e2e/tests/features/settings.spec.ts-snapshots/renders-bounding-boxes-in-debug-mode-1-features-darwin.png b/e2e/tests/features/settings.spec.ts-snapshots/renders-bounding-boxes-in-debug-mode-1-features-darwin.png index 7a20f6e..2e0d133 100644 Binary files a/e2e/tests/features/settings.spec.ts-snapshots/renders-bounding-boxes-in-debug-mode-1-features-darwin.png and b/e2e/tests/features/settings.spec.ts-snapshots/renders-bounding-boxes-in-debug-mode-1-features-darwin.png differ diff --git a/e2e/tests/features/whole-half-quarter-notes.musicxml b/e2e/tests/features/whole-half-quarter-notes.musicxml index ca5cbbe..b1ce647 100644 --- a/e2e/tests/features/whole-half-quarter-notes.musicxml +++ b/e2e/tests/features/whole-half-quarter-notes.musicxml @@ -4,14 +4,38 @@ MuseScore 4.1.1 - 2024-04-01 + 2024-06-09 - - + + + + + 6.99911 + 40 + + + 1596.77 + 1233.87 + + 85.7252 + 85.7252 + 85.7252 + 85.7252 + + + 85.7252 + 85.7252 + 85.7252 + 85.7252 + + + + + Music @@ -29,7 +53,16 @@ - + + + + + 50.00 + -0.00 + + 170.00 + + 1 @@ -45,51 +78,53 @@ 2 - + - F + B + -1 4 1 1 quarter - up + down - + - G + B + -1 4 1 1 quarter - up + down - + - G - 1 + B + -1 4 1 1 quarter - sharp - up + down - + - A + B + -1 4 1 1 quarter - up + down - - + + B -1 @@ -100,23 +135,22 @@ half down - + - D + B -1 - 5 + 4 1 1 quarter - flat down - + - D + B -1 - 5 + 4 1 1 @@ -124,52 +158,17 @@ down - - + + - D - 5 + B + -1 + 4 4 1 whole - natural - - - - - - A - 4 - - 2 - 1 - half - up - - - - A - 4 - - 1 - 1 - quarter - up - - - - C - 5 - - 1 - 1 - quarter - down - - light-heavy - diff --git a/e2e/tests/features/whole-half-quarter-notes.spec.ts-snapshots/renders-notes-whole-half-quarter-correctly-1-features-darwin.png b/e2e/tests/features/whole-half-quarter-notes.spec.ts-snapshots/renders-notes-whole-half-quarter-correctly-1-features-darwin.png index 45ec0a5..e89a472 100644 Binary files a/e2e/tests/features/whole-half-quarter-notes.spec.ts-snapshots/renders-notes-whole-half-quarter-correctly-1-features-darwin.png and b/e2e/tests/features/whole-half-quarter-notes.spec.ts-snapshots/renders-notes-whole-half-quarter-correctly-1-features-darwin.png differ diff --git a/full-featured-editor/src/App.tsx b/full-featured-editor/src/App.tsx index bc73dc2..dd51f21 100644 --- a/full-featured-editor/src/App.tsx +++ b/full-featured-editor/src/App.tsx @@ -5,8 +5,8 @@ import { useDisclosure } from "@mantine/hooks" import { IconActivityHeartbeat, IconArticle, IconMathGreater, IconPlaylist } from "@tabler/icons-react" import classNames from "classnames" import { useState } from "react" -import { Measure } from "./aside-components/Measure" -import { Note } from "./aside-components/Note" +import { Measure } from "./sidebar/measure" +import { Note } from "./sidebar/note" import styles from "./App.module.css" import Container from "./Container" diff --git a/full-featured-editor/src/SelectionProvider.tsx b/full-featured-editor/src/SelectionProvider.tsx index 14a634a..c1b56e7 100644 --- a/full-featured-editor/src/SelectionProvider.tsx +++ b/full-featured-editor/src/SelectionProvider.tsx @@ -1,48 +1,25 @@ -import React, { PropsWithChildren, useContext, useEffect, useState, createContext, useRef, useCallback } from "react" +import React, { PropsWithChildren, useContext, useEffect, useState, createContext } from "react" import { ScoreStormContext } from "./ScoreStormProvider" -import { EventType, GraphicalClef, IGraphical, InteractionEvent } from "@score-storm/core" -import { GraphicalNoteEvent } from "@score-storm/core/dist/graphical/GraphicalNoteEvent" -import { GraphicalRestEvent } from "@score-storm/core/dist/graphical/GraphicalRestEvent" -import { GraphicalTimeSignature } from "@score-storm/core/dist/graphical/GraphicalTimeSignature" +import { EventType, IGraphical, InteractionEvent } from "@score-storm/core" -export enum SelectionType { - Note = "Note", - Rest = "Rest", - Clef = "Clef", - TimeSignature = "TimeSignature", +type SelectionContextValue = { + selectedObject: IGraphical | null } - -type SelectionContextValue = { type: SelectionType | null; getSelectedObject: () => IGraphical | null } -export const SelectionContext = createContext({ type: null, getSelectedObject: () => null }) +export const SelectionContext = createContext({ + selectedObject: null, +}) export const SelectionProvider: React.FC = ({ children }) => { const { scoreStorm } = useContext(ScoreStormContext) - const [selectedType, setSelectedType] = useState(null) - const selectedObjectRef = useRef(null) + const [selectedObject, setSelectedObject] = useState(null) const handleSelect = (event: InteractionEvent) => { - console.log("click", event.object) - if (event.object instanceof GraphicalClef) { - setSelectedType(SelectionType.Clef) - } else if (event.object instanceof GraphicalNoteEvent) { - setSelectedType(SelectionType.Note) - } else if (event.object instanceof GraphicalRestEvent) { - setSelectedType(SelectionType.Rest) - } else if (event.object instanceof GraphicalTimeSignature) { - setSelectedType(SelectionType.TimeSignature) - } else { - setSelectedType(null) - } - selectedObjectRef.current = event.object + setSelectedObject(event.object) } - const getSelectedObject = useCallback(() => { - return selectedObjectRef.current - }, []) - useEffect(() => { scoreStorm.setEventListener(EventType.CLICK, handleSelect) }, []) - return {children} + return {children} } diff --git a/full-featured-editor/src/aside-components/ClefGroup.tsx b/full-featured-editor/src/aside-components/ClefGroup.tsx deleted file mode 100644 index 0e414fa..0000000 --- a/full-featured-editor/src/aside-components/ClefGroup.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { Group, Tooltip, ActionIcon } from "@mantine/core" -import { IconLetterG, IconLetterF } from "@tabler/icons-react" -import { useContext } from "react" -import { ScoreStormContext } from "../ScoreStormProvider" -import { SelectionContext, SelectionType } from "../SelectionProvider" - -export const ClefGroup = () => { - const { scoreStorm } = useContext(ScoreStormContext) - const { type } = useContext(SelectionContext) - - const handleClefClick = () => { - scoreStorm.getScore().setClef() - scoreStorm.render() - } - - if (type !== SelectionType.Clef) { - return null - } - - return ( - - - - - - - - - - - - - - ) -} diff --git a/full-featured-editor/src/aside-components/Measure.tsx b/full-featured-editor/src/aside-components/Measure.tsx deleted file mode 100644 index d1b4c88..0000000 --- a/full-featured-editor/src/aside-components/Measure.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import { ActionIcon, Group, Stack, Tooltip } from "@mantine/core" -import { useContext, useState } from "react" -import { ScoreStormContext } from "../ScoreStormProvider" -import { IconColumnInsertLeft, IconColumnRemove } from "@tabler/icons-react" -import { CollapsibleGroup } from "./CollapsibleGroup" -import { ClefGroup } from "./ClefGroup" - -export const Measure: React.FC = () => { - const { scoreStorm } = useContext(ScoreStormContext) - - const [removeMeasureDisabled, setRemoveMeasureDisabled] = useState(true) - - const updateRemoveMeasureDisabled = () => { - const score = scoreStorm.getScore() - const numberOfMeasures = score.globalMeasures.length - setRemoveMeasureDisabled(numberOfMeasures === 1) - } - - const handleAddMeasureClick = () => { - scoreStorm.getScore().addMeasure() - scoreStorm.render() - updateRemoveMeasureDisabled() - } - - const handleRemoveMeasureClick = () => { - const score = scoreStorm.getScore() - const numberOfMeasures = score.globalMeasures.length - score.removeMeasure(numberOfMeasures - 1) - scoreStorm.render() - updateRemoveMeasureDisabled() - } - - return ( - - - - - - - - - - {!removeMeasureDisabled && ( - - - - - - )} - - - - - - - ) -} diff --git a/full-featured-editor/src/aside-components/Note.tsx b/full-featured-editor/src/aside-components/Note.tsx deleted file mode 100644 index ffe69ee..0000000 --- a/full-featured-editor/src/aside-components/Note.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import { useContext } from "react" -import { ActionIcon, Group, Stack, Tooltip } from "@mantine/core" -import { IconLetterN, IconLetterR } from "@tabler/icons-react" -import { CollapsibleGroup } from "./CollapsibleGroup" -import { SelectionContext, SelectionType } from "../SelectionProvider" -import { ScoreStormContext } from '../ScoreStormProvider' -import { GraphicalNoteEvent } from '@score-storm/core/dist/graphical/GraphicalNoteEvent' - -export const Note: React.FC = () => { - const { scoreStorm } = useContext(ScoreStormContext) - const { type, getSelectedObject } = useContext(SelectionContext) - - const handleClick = () => { - scoreStorm.getScore().swithNoteType((getSelectedObject() as GraphicalNoteEvent).noteEvent) - scoreStorm.render() - } - - - return ( - - - - {type === SelectionType.Note && ( - - - - - - )} - - {type === SelectionType.Rest && ( - - - - - - )} - - - - ) -} diff --git a/full-featured-editor/src/aside-components/icons/FClef.tsx b/full-featured-editor/src/aside-components/icons/FClef.tsx deleted file mode 100644 index aa382e9..0000000 --- a/full-featured-editor/src/aside-components/icons/FClef.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { rem } from "@mantine/core" - -interface FClefIconIconProps extends React.ComponentPropsWithoutRef<"svg"> { - size?: number | string -} - -export function FClefIcon({ size, style, ...others }: FClefIconIconProps) { - return ( - - - - - - - - - - - - - - ) -} diff --git a/full-featured-editor/src/aside-components/icons/GClef.tsx b/full-featured-editor/src/aside-components/icons/GClef.tsx deleted file mode 100644 index f34b607..0000000 --- a/full-featured-editor/src/aside-components/icons/GClef.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { rem } from "@mantine/core" - -interface GClefIconIconProps extends React.ComponentPropsWithoutRef<"svg"> { - size?: number | string -} - -export function GClefIcon({ size, style, ...others }: GClefIconIconProps) { - return ( - - - - - - - ) -} diff --git a/full-featured-editor/src/aside-components/icons/QuarterNote.tsx b/full-featured-editor/src/aside-components/icons/QuarterNote.tsx deleted file mode 100644 index bc97bde..0000000 --- a/full-featured-editor/src/aside-components/icons/QuarterNote.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { rem } from "@mantine/core" - -interface QuarterNoteIconProps extends React.ComponentPropsWithoutRef<"svg"> { - size?: number | string -} - -export function QuarterNoteIcon({ size, style, ...others }: QuarterNoteIconProps) { - return ( - - - - - - - ) -} diff --git a/full-featured-editor/src/aside-components/icons/QuarterRest.tsx b/full-featured-editor/src/aside-components/icons/QuarterRest.tsx deleted file mode 100644 index 603b0e0..0000000 --- a/full-featured-editor/src/aside-components/icons/QuarterRest.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { rem } from "@mantine/core" - -interface QuarterRestIconProps extends React.ComponentPropsWithoutRef<"svg"> { - size?: number | string -} - -export function QuarterRestIcon({ size, style, ...others }: QuarterRestIconProps) { - return ( - - - - ) -} diff --git a/full-featured-editor/src/sidebar/ActionButton.module.css b/full-featured-editor/src/sidebar/ActionButton.module.css new file mode 100644 index 0000000..055548d --- /dev/null +++ b/full-featured-editor/src/sidebar/ActionButton.module.css @@ -0,0 +1,18 @@ +.actionButton { + color: var(--mantine-color-blue-9); + background-color: var(--mantine-color-blue-1); + &:hover { + background-color: darken(var(--mantine-color-blue-1), 0.02); + color: var(--mantine-color-blue-9); + } + + &[data-active] { + border: 1px solid var(--mantine-color-blue-9); + border-width: 1.2px; + } + + &[data-disabled] { + color: #6e7a86; + background: #d9e7f1; + } +} diff --git a/full-featured-editor/src/sidebar/ActionButton.tsx b/full-featured-editor/src/sidebar/ActionButton.tsx new file mode 100644 index 0000000..f9faf1b --- /dev/null +++ b/full-featured-editor/src/sidebar/ActionButton.tsx @@ -0,0 +1,33 @@ +import { FunctionComponent, forwardRef } from "react" +import { ActionIcon } from "@mantine/core" +import { IconProps } from "@tabler/icons-react" +import styles from "./ActionButton.module.css" + +export interface CustomIconProps extends React.ComponentPropsWithoutRef<"svg"> { + size?: number | string +} + +type ActionButtonProps = { + Icon: FunctionComponent> | FunctionComponent + active?: boolean + disabled?: boolean + onClick?: React.MouseEventHandler +} + +export const ActionButton = forwardRef( + ({ Icon, active, disabled, onClick }, ref) => { + return ( + + + + ) + }, +) diff --git a/full-featured-editor/src/aside-components/CollapsibleGroup.module.css b/full-featured-editor/src/sidebar/CollapsibleGroup.module.css similarity index 100% rename from full-featured-editor/src/aside-components/CollapsibleGroup.module.css rename to full-featured-editor/src/sidebar/CollapsibleGroup.module.css diff --git a/full-featured-editor/src/aside-components/CollapsibleGroup.tsx b/full-featured-editor/src/sidebar/CollapsibleGroup.tsx similarity index 100% rename from full-featured-editor/src/aside-components/CollapsibleGroup.tsx rename to full-featured-editor/src/sidebar/CollapsibleGroup.tsx diff --git a/full-featured-editor/src/sidebar/icons/DoubleFlat.tsx b/full-featured-editor/src/sidebar/icons/DoubleFlat.tsx new file mode 100644 index 0000000..d6aa2a1 --- /dev/null +++ b/full-featured-editor/src/sidebar/icons/DoubleFlat.tsx @@ -0,0 +1,21 @@ +import { rem } from "@mantine/core" +import { CustomIconProps } from "../ActionButton" + +export const DoubleFlatIcon: React.FC = ({ size, style, ...others }) => { + return ( + + + + + ) +} diff --git a/full-featured-editor/src/sidebar/icons/FClef.tsx b/full-featured-editor/src/sidebar/icons/FClef.tsx new file mode 100644 index 0000000..0086d81 --- /dev/null +++ b/full-featured-editor/src/sidebar/icons/FClef.tsx @@ -0,0 +1,77 @@ +import { rem } from "@mantine/core" + +interface FClefIconProps extends React.ComponentPropsWithoutRef<"svg"> { + size?: number | string +} + +export function FClefIcon({ size, style, ...others }: FClefIconProps) { + return ( + + {/* + + */} + {/* + + */} + + + + + + ) +} diff --git a/full-featured-editor/src/sidebar/icons/Flat.tsx b/full-featured-editor/src/sidebar/icons/Flat.tsx new file mode 100644 index 0000000..931e89b --- /dev/null +++ b/full-featured-editor/src/sidebar/icons/Flat.tsx @@ -0,0 +1,20 @@ +import { rem } from "@mantine/core" +import { CustomIconProps } from "../ActionButton" + +export const FlatIcon: React.FC = ({ size, style, ...others }) => { + return ( + + + + ) +} diff --git a/full-featured-editor/src/sidebar/icons/GClef.tsx b/full-featured-editor/src/sidebar/icons/GClef.tsx new file mode 100644 index 0000000..fd33de4 --- /dev/null +++ b/full-featured-editor/src/sidebar/icons/GClef.tsx @@ -0,0 +1,27 @@ +import { rem } from "@mantine/core" + +interface GClefIconProps extends React.ComponentPropsWithoutRef<"svg"> { + size?: number | string +} + +export function GClefIcon({ size, style, ...others }: GClefIconProps) { + return ( + + {/* */} + + + ) +} diff --git a/full-featured-editor/src/sidebar/icons/Natural.tsx b/full-featured-editor/src/sidebar/icons/Natural.tsx new file mode 100644 index 0000000..53718a2 --- /dev/null +++ b/full-featured-editor/src/sidebar/icons/Natural.tsx @@ -0,0 +1,24 @@ +import { rem } from "@mantine/core" +import { CustomIconProps } from "../ActionButton" + +export const NaturalIcon: React.FC = ({ size, style, ...others }) => { + return ( + + + + + ) +} diff --git a/full-featured-editor/src/sidebar/measure/ClefGroup.tsx b/full-featured-editor/src/sidebar/measure/ClefGroup.tsx new file mode 100644 index 0000000..5921ca9 --- /dev/null +++ b/full-featured-editor/src/sidebar/measure/ClefGroup.tsx @@ -0,0 +1,35 @@ +import { Group, Tooltip } from "@mantine/core" +import { useContext } from "react" +import { ScoreStormContext } from "../../ScoreStormProvider" +import { SelectionContext } from "../../SelectionProvider" +import { GClefIcon } from "../icons/GClef" +import { FClefIcon } from "../icons/FClef" +import { ActionButton } from "../ActionButton" +import { GraphicalClef } from "@score-storm/core" + +export const ClefGroup = () => { + const { scoreStorm } = useContext(ScoreStormContext) + const { selectedObject } = useContext(SelectionContext) + + const handleClefClick = () => { + scoreStorm.getScore().setClef() + scoreStorm.render() + } + + const isClef = selectedObject instanceof GraphicalClef + + const gClefActive = isClef && (selectedObject as GraphicalClef).clef.sign === "G" + const fClefActive = isClef && (selectedObject as GraphicalClef).clef.sign === "F" + + return ( + + + + + + + + + + ) +} diff --git a/full-featured-editor/src/sidebar/measure/ManagementGroup.tsx b/full-featured-editor/src/sidebar/measure/ManagementGroup.tsx new file mode 100644 index 0000000..6668092 --- /dev/null +++ b/full-featured-editor/src/sidebar/measure/ManagementGroup.tsx @@ -0,0 +1,48 @@ +import { ActionIcon, Group, Tooltip } from "@mantine/core" +import { useContext, useState } from "react" +import { ScoreStormContext } from "../../ScoreStormProvider" +import { IconColumnInsertLeft, IconColumnRemove } from "@tabler/icons-react" + +export const ManagementGroup: React.FC = () => { + const { scoreStorm } = useContext(ScoreStormContext) + + const [removeMeasureDisabled, setRemoveMeasureDisabled] = useState(true) + + const updateRemoveMeasureDisabled = () => { + const score = scoreStorm.getScore() + const numberOfMeasures = score.globalMeasures.length + setRemoveMeasureDisabled(numberOfMeasures === 1) + } + + const handleAddMeasureClick = () => { + scoreStorm.getScore().addMeasure() + scoreStorm.render() + updateRemoveMeasureDisabled() + } + + const handleRemoveMeasureClick = () => { + const score = scoreStorm.getScore() + const numberOfMeasures = score.globalMeasures.length + score.removeMeasure(numberOfMeasures - 1) + scoreStorm.render() + updateRemoveMeasureDisabled() + } + + return ( + + + + + + + + {!removeMeasureDisabled && ( + + + + + + )} + + ) +} diff --git a/full-featured-editor/src/sidebar/measure/index.tsx b/full-featured-editor/src/sidebar/measure/index.tsx new file mode 100644 index 0000000..b9b45fc --- /dev/null +++ b/full-featured-editor/src/sidebar/measure/index.tsx @@ -0,0 +1,18 @@ +import { Stack } from "@mantine/core" +import { CollapsibleGroup } from "../CollapsibleGroup" +import { ClefGroup } from "./ClefGroup" +import { ManagementGroup } from './ManagementGroup' + +export const Measure: React.FC = () => { + + return ( + + + + + + + + + ) +} diff --git a/full-featured-editor/src/sidebar/note/AccidentalsGroup.tsx b/full-featured-editor/src/sidebar/note/AccidentalsGroup.tsx new file mode 100644 index 0000000..d796fe6 --- /dev/null +++ b/full-featured-editor/src/sidebar/note/AccidentalsGroup.tsx @@ -0,0 +1,69 @@ +import { useContext } from "react" +import { Group } from "@mantine/core" +import { IconHash, IconLetterX } from "@tabler/icons-react" +import { SelectionContext } from "../../SelectionProvider" +import { FlatIcon } from "../icons/Flat" +import { DoubleFlatIcon } from "../icons/DoubleFlat" +import { NaturalIcon } from "../icons/Natural" +import { ActionButton } from "../ActionButton" +import { GraphicalNoteEvent } from "@score-storm/core" +import { ScoreStormContext } from "../../ScoreStormProvider" + +export const AccidentalsGroup: React.FC = () => { + const { selectedObject } = useContext(SelectionContext) + const { scoreStorm } = useContext(ScoreStormContext) + + const handleSetAccidental = (accidental?: number) => { + const note = (selectedObject as GraphicalNoteEvent).noteEvent?.notes![0] + scoreStorm.getScore().changeNoteAccidental(note, accidental) + scoreStorm.render() + } + + const accidentalsAvailable = selectedObject instanceof GraphicalNoteEvent + + let showAccidentals = false + let alter: number | undefined + + if (accidentalsAvailable) { + const firstNoteInEvent = (selectedObject as GraphicalNoteEvent).noteEvent?.notes![0] + showAccidentals = !!firstNoteInEvent.accidentalDisplay?.show + if (showAccidentals) { + alter = firstNoteInEvent.pitch.alter || 0 + } + } + + return ( + + handleSetAccidental(1)} + /> + handleSetAccidental(-1)} + /> + handleSetAccidental(0)} + /> + handleSetAccidental(2)} + /> + handleSetAccidental(-2)} + /> + + ) +} diff --git a/full-featured-editor/src/sidebar/note/TypeGroup.tsx b/full-featured-editor/src/sidebar/note/TypeGroup.tsx new file mode 100644 index 0000000..1b04054 --- /dev/null +++ b/full-featured-editor/src/sidebar/note/TypeGroup.tsx @@ -0,0 +1,37 @@ +import { useContext } from "react" +import { ActionIcon, Group, Tooltip } from "@mantine/core" +import { IconLetterN, IconLetterR } from "@tabler/icons-react" +import { SelectionContext } from "../../SelectionProvider" +import { ScoreStormContext } from "../../ScoreStormProvider" +import { GraphicalNoteEvent } from "@score-storm/core" +import { GraphicalRestEvent } from "@score-storm/core" + +export const TypeGroup: React.FC = () => { + const { scoreStorm } = useContext(ScoreStormContext) + const { selectedObject } = useContext(SelectionContext) + + const handleClick = () => { + scoreStorm.getScore().swithNoteType((selectedObject as GraphicalNoteEvent).noteEvent) + scoreStorm.render() + } + + return ( + + {selectedObject instanceof GraphicalNoteEvent && ( + + + + + + )} + + {selectedObject instanceof GraphicalRestEvent && ( + + + + + + )} + + ) +} diff --git a/full-featured-editor/src/sidebar/note/index.tsx b/full-featured-editor/src/sidebar/note/index.tsx new file mode 100644 index 0000000..6ddf08a --- /dev/null +++ b/full-featured-editor/src/sidebar/note/index.tsx @@ -0,0 +1,17 @@ +import { Stack } from "@mantine/core" +import { CollapsibleGroup } from "../CollapsibleGroup" +import { AccidentalsGroup } from "./AccidentalsGroup" +import { TypeGroup } from "./TypeGroup" + +export const Note: React.FC = () => { + return ( + + + + + + + + + ) +} diff --git a/packages/canvas-renderer/src/Layer.ts b/packages/canvas-renderer/src/Layer.ts index b7aec06..eda98be 100644 --- a/packages/canvas-renderer/src/Layer.ts +++ b/packages/canvas-renderer/src/Layer.ts @@ -1,15 +1,15 @@ export class Layer { canvasElement!: HTMLCanvasElement context!: CanvasRenderingContext2D - constructor(containerElement: HTMLDivElement, disablePointerEvents = false) { + constructor(containerElement: HTMLDivElement, isInteractionsLayer = false) { this.canvasElement = document.createElement("canvas") this.context = this.canvasElement.getContext("2d")! containerElement.appendChild(this.canvasElement) - this.canvasElement.style.setProperty("position", "absolute") - this.canvasElement.style.setProperty("left", "0") - this.canvasElement.style.setProperty("top", "0") - if (disablePointerEvents) { + if (isInteractionsLayer) { + this.canvasElement.style.setProperty("position", "absolute") + this.canvasElement.style.setProperty("left", "0") + this.canvasElement.style.setProperty("top", "0") this.canvasElement.style.setProperty("pointer-events", "none") } } diff --git a/packages/core/src/graphical/GraphicalClef.ts b/packages/core/src/graphical/GraphicalClef.ts index fd9bf1c..9e585d4 100644 --- a/packages/core/src/graphical/GraphicalClef.ts +++ b/packages/core/src/graphical/GraphicalClef.ts @@ -12,6 +12,7 @@ export class GraphicalClef extends BaseGraphical implements IGraphical { verticalShift: number // value in stave spaces x!: number // todo : create Poind2d type y!: number + clef: Clef static glyphMap = { G: ClefG, @@ -20,6 +21,7 @@ export class GraphicalClef extends BaseGraphical implements IGraphical { constructor(clef: Clef) { super() + this.clef = clef const { position, sign } = clef this.verticalShift = position * -0.5 diff --git a/packages/core/src/graphical/GraphicalNoteEvent.ts b/packages/core/src/graphical/GraphicalNoteEvent.ts index 4e7889a..5639b8c 100644 --- a/packages/core/src/graphical/GraphicalNoteEvent.ts +++ b/packages/core/src/graphical/GraphicalNoteEvent.ts @@ -1,11 +1,13 @@ import { Settings } from "../BaseRenderer" import { IRenderer } from "../interfaces" -import { NoteEvent } from "../model/Measure" +import { Note, NoteEvent } from "../model/Measure" import { BaseGraphical } from "./BaseGraphical" +import { DoubleFlat, DoubleSharp, Flat, Natural, Sharp } from "./glyphs/accidental" import { NoteheadHalf, NoteheadQuarter, NoteheadWhole } from "./glyphs/notehead" import { BBox, Glyph, IGraphical } from "./interfaces" export class GraphicalNoteEvent extends BaseGraphical implements IGraphical { + noteEvent: NoteEvent height!: number width!: number noteheadGlyph: Glyph @@ -13,7 +15,8 @@ export class GraphicalNoteEvent extends BaseGraphical implements IGraphical { drawStem!: boolean x!: number y!: number - noteEvent: NoteEvent + accidentalGlyph?: Glyph + accidentalWidth?: number static glyphMap = { whole: NoteheadWhole, @@ -21,6 +24,14 @@ export class GraphicalNoteEvent extends BaseGraphical implements IGraphical { quarter: NoteheadQuarter, } + static glyphAccidentalMap = { + [-2]: DoubleFlat, + [-1]: Flat, + [0]: Natural, + [1]: Sharp, + [2]: DoubleSharp, + } + constructor(noteEvent: NoteEvent) { super() this.noteEvent = noteEvent @@ -30,6 +41,19 @@ export class GraphicalNoteEvent extends BaseGraphical implements IGraphical { this.drawStem = true } + // for now only first note in chord + const firstNote = noteEvent.notes![0] + const showAccidentals = !!firstNote.accidentalDisplay?.show + const accidental = firstNote.pitch.alter || 0 + const accidentalGlyph = + GraphicalNoteEvent.glyphAccidentalMap[accidental as keyof typeof GraphicalNoteEvent.glyphAccidentalMap] + if (typeof accidental === "number" && !accidentalGlyph) { + throw new Error(`Invalid accidental ${accidental}`) + } + if (showAccidentals) { + this.accidentalGlyph = accidentalGlyph + } + this.verticalShift = 0 // TODO: respect pitch this.noteheadGlyph = GraphicalNoteEvent.glyphMap[duration as keyof typeof GraphicalNoteEvent.glyphMap] @@ -46,6 +70,11 @@ export class GraphicalNoteEvent extends BaseGraphical implements IGraphical { const glyphWidth = this.noteheadGlyph.bBoxes.bBoxNE[0] - this.noteheadGlyph.bBoxes.bBoxSW[0] this.width = glyphWidth + + if (this.accidentalGlyph) { + const accidentalGlyphWidth = this.accidentalGlyph.bBoxes.bBoxNE[0] - this.accidentalGlyph.bBoxes.bBoxSW[0] + this.accidentalWidth = accidentalGlyphWidth + } } setCoordinates(x: number, y: number, settings: Settings): void { @@ -54,18 +83,31 @@ export class GraphicalNoteEvent extends BaseGraphical implements IGraphical { } getBBox(settings: Settings): BBox { + let xShift = 0 + if (this.accidentalGlyph) { + xShift = xShift + this.accidentalWidth! * settings.unit + 0.5 * settings.unit + } return { - x: this.x, - y: this.y - this.noteheadGlyph.bBoxes.bBoxNE[1] * settings.unit, + x: this.x + xShift, + y: this.y - this.noteheadGlyph.bBoxes.bBoxNE[1] * settings.unit , width: this.width * settings.unit, height: this.height * settings.unit, } } render(renderer: IRenderer, settings: Settings) { + let xShift = 0 + if (this.accidentalGlyph) { + renderer.drawGlyph( + this.getTextFromUnicode(this.accidentalGlyph.symbol), + this.x - this.accidentalGlyph.bBoxes.bBoxSW[0] * settings.unit, + this.y, + ) + xShift = xShift + this.accidentalWidth! * settings.unit + 0.5 * settings.unit + } renderer.drawGlyph( this.getTextFromUnicode(this.noteheadGlyph.symbol), - this.x - this.noteheadGlyph.bBoxes.bBoxSW[0] * settings.unit, + this.x - this.noteheadGlyph.bBoxes.bBoxSW[0] * settings.unit + xShift, this.y, ) @@ -74,7 +116,7 @@ export class GraphicalNoteEvent extends BaseGraphical implements IGraphical { const stemHeight = 3.5 * settings.unit const stemHeightCut = 0.17 * settings.unit renderer.drawRect( - this.x + this.width * settings.unit - stemThickness, + this.x + this.width * settings.unit - stemThickness + xShift, this.y - stemHeight, stemThickness, stemHeight - stemHeightCut, diff --git a/packages/core/src/graphical/glyphs/accidental/index.ts b/packages/core/src/graphical/glyphs/accidental/index.ts new file mode 100644 index 0000000..8366683 --- /dev/null +++ b/packages/core/src/graphical/glyphs/accidental/index.ts @@ -0,0 +1,32 @@ +import { Glyph } from "../../interfaces" + +export const Flat: Glyph = { + symbol: "U+E260", + smuflName: "accidentalFlat", + advancedWidth: 0.904, + bBoxes: { bBoxNE: [0.904, 1.756], bBoxSW: [0, -0.7] }, +} +export const Natural: Glyph = { + symbol: "U+E261", + smuflName: "accidentalNatural", + advancedWidth: 0.672, + bBoxes: { bBoxNE: [0.672, 1.364], bBoxSW: [0, -1.34] }, +} +export const Sharp: Glyph = { + symbol: "U+E262", + smuflName: "accidentalSharp", + advancedWidth: 0.996, + bBoxes: { bBoxNE: [0.996, 1.4], bBoxSW: [0, -1.392] }, +} +export const DoubleSharp: Glyph = { + symbol: "U+E263", + smuflName: "accidentalDoubleSharp", + advancedWidth: 1, + bBoxes: { bBoxNE: [0.988, 0.508], bBoxSW: [0, -0.5] }, +} +export const DoubleFlat: Glyph = { + symbol: "U+E264", + smuflName: "accidentalDoubleFlat", + advancedWidth: 1.652, + bBoxes: { bBoxNE: [1.644, 1.748], bBoxSW: [0, -0.7] }, +} diff --git a/packages/core/src/graphical/index.ts b/packages/core/src/graphical/index.ts index 1f51d3a..7f92fd2 100644 --- a/packages/core/src/graphical/index.ts +++ b/packages/core/src/graphical/index.ts @@ -1,3 +1,6 @@ export { GraphicalScore } from "./GraphicalScore" export { GraphicalClef } from "./GraphicalClef" +export { GraphicalRestEvent } from "./GraphicalRestEvent" +export { GraphicalNoteEvent } from "./GraphicalNoteEvent" +export { GraphicalTimeSignature } from "./GraphicalTimeSignature" export { type IGraphical, type BBox } from "./interfaces" diff --git a/packages/core/src/model/Measure.ts b/packages/core/src/model/Measure.ts index 9546590..69dd1a8 100644 --- a/packages/core/src/model/Measure.ts +++ b/packages/core/src/model/Measure.ts @@ -3,7 +3,10 @@ export type Note = { alter?: number octave: number step: string - } + }, + accidentalDisplay?: { + show: boolean + }, } export type NoteDuration = { diff --git a/packages/core/src/model/Score.ts b/packages/core/src/model/Score.ts index 12a41ae..fc488e1 100644 --- a/packages/core/src/model/Score.ts +++ b/packages/core/src/model/Score.ts @@ -1,6 +1,6 @@ import { getMNXScore, getScoreFromMusicXml } from "mnxconverter" import { GlobalMeasure, TimeSignature } from "./GlobalMeasure" -import { Measure, NoteEvent } from "./Measure" +import { Measure, Note, NoteEvent } from "./Measure" export type QuickScoreOptions = { numberOfMeasures?: number @@ -148,8 +148,30 @@ export class Score { swithNoteType(noteEvent: NoteEvent) { if (noteEvent.rest) { noteEvent.rest = undefined + noteEvent.notes = [ + { + pitch: { + octave: 4, + step: "B", + }, + }, + ] } else { noteEvent.rest = {} + noteEvent.notes = undefined } } + + changeNoteAccidental(note: Note, newAlter?: number) { + // taking into account key signature is out of scope for now + const { alter, ...rest } = note.pitch + const alterChanged = alter !== newAlter + if(!alterChanged) { + newAlter = undefined + } + note.accidentalDisplay = { + show: typeof newAlter === "number", + } + note.pitch = { ...rest, ...(newAlter !== 0 && { alter: newAlter }) } + } }