Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ development artifact builds use `vMAJOR.MINOR.PATCH-dev.N`.
- Added Leland glyph duration controls to the Notation window and main Notation track, with 4/5/6/7 shortcuts for eighth, quarter, half, and whole notes.
- Added selectable Notation rest durations with persisted measure layouts and split-rest MusicXML export.
- Added MusicXML export metadata, project tempo marking, and boxed bold Region labels.
- Fixed Notation duration button glyph alignment in the timeline and Notation window.
- Fixed MusicXML export so harmony offsets are relative to the current notation position and exported notes include type values.
- Added MusicXML export for Notation from the File menu and Notation window.
- Added a collapsible Notation track in the main timeline, collapsed by default and saved per project.
Expand Down
12 changes: 12 additions & 0 deletions JammLab/Services/NotationSMuFL.swift
Original file line number Diff line number Diff line change
Expand Up @@ -137,4 +137,16 @@ enum NotationMusicFontRegistry {
struct NotationSMuFLGlyphPath {
var path: CGPath
var bounds: CGRect

func centeredTransform(in size: CGSize) -> CGAffineTransform {
// CoreText glyph paths use a y-up coordinate space; SwiftUI Canvas draws y-down.
CGAffineTransform(
a: 1,
b: 0,
c: 0,
d: -1,
tx: size.width / 2 - bounds.midX,
ty: size.height / 2 + bounds.midY
)
}
}
38 changes: 32 additions & 6 deletions JammLab/Views/Components/AppControls.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,10 @@ struct NotationDurationControl: View {
return Button {
denominator = option.denominator
} label: {
Text(option.symbol.glyph)
.font(.custom(
NotationMusicFontRegistry.fontName,
size: AppTheme.ControlSize.notationDurationGlyphSize
))
.foregroundStyle(iconColor(isSelected: isSelected))
NotationDurationGlyphView(
symbol: option.symbol,
color: iconColor(isSelected: isSelected)
)
.frame(
width: AppTheme.ControlSize.notationDurationButtonWidth,
height: AppTheme.ControlSize.notationDurationControlHeight
Expand Down Expand Up @@ -84,6 +82,34 @@ struct NotationDurationControl: View {
}
}

private struct NotationDurationGlyphView: View {
let symbol: NotationDurationControlSymbol
let color: Color

var body: some View {
ZStack {
if let glyphPath = NotationMusicFontRegistry.glyphPath(
for: symbol,
fontSize: AppTheme.ControlSize.notationDurationGlyphSize
) {
Canvas { context, size in
context.fill(
Path(glyphPath.path).applying(glyphPath.centeredTransform(in: size)),
with: .color(color)
)
}
} else {
Text(symbol.glyph)
.font(.custom(
NotationMusicFontRegistry.fontName,
size: AppTheme.ControlSize.notationDurationGlyphSize
))
.foregroundStyle(color)
}
}
}
}

private struct NotationDurationOption: Identifiable {
let denominator: Int
let durationName: String
Expand Down
25 changes: 25 additions & 0 deletions JammLabTests/NotationPrimitivesTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,31 @@ final class NotationPrimitivesTests: XCTestCase {
}
}

func testLelandDurationControlGlyphPathsCenterInsideButtonFrame() throws {
let buttonSize = CGSize(
width: AppTheme.ControlSize.notationDurationButtonWidth,
height: AppTheme.ControlSize.notationDurationControlHeight
)

for symbol in [
NotationDurationControlSymbol.whole,
.half,
.quarter,
.eighth
] {
let glyphPath = try XCTUnwrap(NotationMusicFontRegistry.glyphPath(
for: symbol,
fontSize: AppTheme.ControlSize.notationDurationGlyphSize
))
let centeredBounds = glyphPath.bounds.applying(glyphPath.centeredTransform(in: buttonSize))

XCTAssertEqual(centeredBounds.midX, buttonSize.width / 2, accuracy: 0.0001)
XCTAssertEqual(centeredBounds.midY, buttonSize.height / 2, accuracy: 0.0001)
XCTAssertLessThanOrEqual(centeredBounds.width, buttonSize.width)
XCTAssertLessThanOrEqual(centeredBounds.height, buttonSize.height)
}
}

func testWholeRestVisualCenterUsesStandardStaffPosition() {
let y = NotationMeasureLayout.wholeRestVisualCenterY(
staffTop: 10,
Expand Down