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 }) }
+ }
}