Skip to content

Commit 309d1da

Browse files
committed
Implements #17 Print arrangement (grid or notes)
Scores can now be printed and exported as PDF files Signed-off-by: Mike Lischke <mike@lischke-online.de>
1 parent 8b8c210 commit 309d1da

28 files changed

Lines changed: 1614 additions & 248 deletions

.vscode/settings.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
"npx vitest": true,
1515
"npx playwright": true,
1616
"git checkout": true,
17-
"npx tsx": true
17+
"npx tsx": true,
18+
"npx vite": true
1819
},
1920
}

build/fix-svg-attributes.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -106,13 +106,16 @@ const fixSvgFile = (filePath: string): void => {
106106
});
107107

108108
if (fileName === "note.svg") {
109-
// Restore manual stroke attributes on stems and fix attribute order.
110-
// Drawing programs swap shape-rendering/vector-effect and move id earlier.
109+
// Restore manual stroke attributes on stems and fix attribute order. Drawing
110+
// programs may move `id` earlier or re-introduce `shape-rendering` /
111+
// `vector-effect` that we deliberately removed: those make the printed stem
112+
// dotted on hi-DPI printers (`crispEdges` aliasing + non-scaling 1.45px).
113+
// Strip both and use a thicker scaling stroke instead.
111114
root.querySelectorAll('[id$="-stem"]').forEach((el) => {
112-
el.setAttribute("stroke-width", "1.45");
115+
el.removeAttribute("shape-rendering");
116+
el.removeAttribute("vector-effect");
117+
el.setAttribute("stroke-width", "3.5");
113118
reorderAttributes(el, [
114-
{ name: "shape-rendering", value: "crispEdges" },
115-
{ name: "vector-effect", value: "non-scaling-stroke" },
116119
{ name: "id" },
117120
{ name: "style" }, // placeholder position; overwritten by styleRules below
118121
]);

cspell.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
"Sete",
5959
"Shekere",
6060
"spacebar",
61+
"stylesheet",
6162
"Surdo",
6263
"surdos",
6364
"tabview",

src/App.scss

Lines changed: 97 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*/
55

6+
@use "grid-shared" as score;
7+
68
@font-face {
79
font-family: "Fairytale";
810
src: url("/fonts/FairytaleSerif-MediumItalic.woff2") format("woff2");
@@ -240,78 +242,7 @@ h6 {
240242
transition: left 0.050s linear;
241243
}
242244

243-
.grid-measure-viewer {
244-
border-right: 2px solid var(--color-base-300);
245-
}
246-
247-
.grid-measure-beam {
248-
height: 20pt;
249-
margin-bottom: 5px;
250-
position: relative;
251-
overflow: hidden;
252-
253-
// Horizontal beam; left/right endpoints are set via CSS vars from GridMeasureBeam.tsx
254-
// so that edge measures start/end exactly at the measured beat positions.
255-
&::before {
256-
content: '';
257-
position: absolute;
258-
top: 0;
259-
left: var(--beam-left, 0);
260-
right: var(--beam-right, 0);
261-
height: 5px;
262-
background-color: var(--color-base-content);
263-
}
264-
}
265-
266-
.grid-measure-number {
267-
position: absolute;
268-
top: 10px;
269-
font-size: 11px;
270-
color: var(--color-base-content);
271-
pointer-events: none;
272-
}
273-
274-
.grid-measure-beat {
275-
position: absolute;
276-
top: 0;
277-
height: 100%;
278-
border-left: 1px solid var(--color-base-content);
279-
280-
&.bar-start {
281-
border-left-width: 2px;
282-
}
283-
}
284-
285-
.grid-measure-row {
286-
margin: 2px 8px 14px 8px;
287-
}
288-
289-
.subdivision {
290-
position: relative;
291-
padding: 4px 0 4px 0;
292-
293-
// Bracket color: neutral gray, level 1 ≈ #ccc (83 % L) getting 15 % darker
294-
// per nesting level. Fixed lightness guarantees light → dark on every theme.
295-
--subdivision-color: oklch(calc(83% - (var(--current-level, 1) - 1) * 15%) 0 0);
296-
297-
&::before {
298-
content: '';
299-
position: absolute;
300-
top: 2px;
301-
left: 0;
302-
right: 0;
303-
bottom: 2px;
304-
border-top: 3px solid var(--subdivision-color);
305-
border-bottom: 3px solid var(--subdivision-color);
306-
border-radius: 6px;
307-
pointer-events: none;
308-
z-index: 1;
309-
}
310-
311-
> .note-viewer {
312-
height: calc(var(--note-height) - var(--current-level) * 8px);
313-
}
314-
}
245+
@include score.score-grid-rules($useThemeColors: true, $renderBeam: true, $beatNumberTop: 10px);
315246
}
316247
}
317248

@@ -320,7 +251,7 @@ h6 {
320251
--track-controls-expanded-width: clamp(200px, 24vw, 300px);
321252

322253
overflow: hidden;
323-
gap: 12px;
254+
gap: 8px;
324255

325256
flex: 0 0 var(--track-controls-collapsed-width);
326257
width: var(--track-controls-expanded-width);
@@ -424,14 +355,38 @@ h6 {
424355
justify-content: flex-start;
425356
}
426357

427-
.trackViewModeToggle {
358+
.trackViewModeToggleGroup {
428359
margin-left: 8px;
429360
}
430361

362+
.trackViewModeIcons {
363+
height: 32px;
364+
margin-top: 3px;
365+
}
366+
367+
.trackViewModeGridIcon {
368+
width: 8px;
369+
height: 8px;
370+
flex: 0 0 auto;
371+
border: 1px dashed currentColor;
372+
}
373+
374+
.trackViewModeToggle {
375+
margin: 0;
376+
}
377+
431378
.trackViewModeToggle input[type="checkbox"] {
432379
width: 32px;
433380
height: 18px;
434381
}
382+
383+
.trackViewModeToggle.toggle-vertical {
384+
// daisyUI's toggle is horizontal. Rotate it 90° clockwise so the handle
385+
// moves vertically. With this rotation, the unchecked handle sits at the
386+
// top and the checked handle at the bottom.
387+
transform: rotate(90deg);
388+
margin-right: -2px;
389+
}
435390
}
436391

437392
.guiderail {
@@ -923,9 +878,8 @@ h6 {
923878
color: var(--color-base-content);
924879
}
925880

926-
/* ---- Slap: cross inside square (uses ::before so ::after keeps the filled square) ---- */
927-
&.slap::before {
928-
content: "";
881+
/* ---- Slap: white cross SVG drawn on top of the filled square head ---- */
882+
.staff-note-head-slap-svg {
929883
position: absolute;
930884
left: calc(50% - 7px);
931885
bottom: 20px;
@@ -934,14 +888,10 @@ h6 {
934888
transform: translate(-50%, -30%);
935889
pointer-events: none;
936890
z-index: 1;
937-
background-image:
938-
linear-gradient(45deg,
939-
transparent 42%, var(--color-base-100) 42%, var(--color-base-100) 58%, transparent 58%),
940-
linear-gradient(-45deg,
941-
transparent 42%, var(--color-base-100) 42%, var(--color-base-100) 58%, transparent 58%);
891+
overflow: visible;
942892
}
943893

944-
/* ---- Hand + Cross → hollow square around the cross (Kessel) ---- */
894+
/* ---- Hand + Cross → hollow square around the cross (body) ---- */
945895
&.body-hand::before {
946896
content: "";
947897
position: absolute;
@@ -1126,6 +1076,14 @@ h6 {
11261076
border-radius: 8px;
11271077
}
11281078

1079+
/*
1080+
* In the grid view, the row is a CSS grid with `baseSteps` columns of `1fr`,
1081+
* and tuplet subdivisions span `subdivision.normal` columns while internally
1082+
* being grids of `subdivision.actual` columns. This makes a tuplet's outer
1083+
* width exactly equal to `normal` base cells regardless of margin, padding,
1084+
* border or grid `gap`, since `gap` is included in the spanned width.
1085+
*/
1086+
11291087
.note-viewer:active .hold-detector {
11301088
opacity: 30%;
11311089
}
@@ -1535,31 +1493,68 @@ h6 {
15351493
height: 100%;
15361494
}
15371495

1538-
#settingsDialog #settingsGrid {
1496+
/*
1497+
* macOS-style settings card. A rounded container with horizontal hairline
1498+
* dividers between rows. Each row has its label on the left and its control
1499+
* on the right.
1500+
*/
1501+
.settings-card {
15391502
margin: 16px 0;
1503+
background-color: color-mix(in srgb, var(--color-base-content) 4%, var(--color-base-100));
1504+
border: 1px solid var(--color-base-300);
1505+
border-radius: 10px;
1506+
overflow: hidden;
1507+
}
15401508

1541-
.settingName {
1542-
justify-content: center;
1543-
}
1509+
.settings-row {
1510+
padding: 10px 16px;
1511+
min-height: 36px;
1512+
border-bottom: 1px solid var(--color-base-300);
15441513

1545-
.settingValue {
1546-
align-items: center;
1547-
justify-content: flex-end;
1514+
&:last-child {
1515+
border-bottom: 0;
15481516
}
1517+
}
15491518

1550-
.zoomButton {
1551-
border: none;
1552-
width: 32px;
1553-
height: 32px;
1554-
margin: 0 2px;
1555-
}
1519+
.settings-row-stacked {
1520+
gap: 6px;
1521+
}
15561522

1557-
.resetButton {
1558-
border: none;
1559-
height: 32px;
1560-
margin-left: 8px;
1561-
padding: 0 12px;
1562-
}
1523+
.settings-row-stacked > .settings-row-label {
1524+
margin-right: 0;
1525+
}
1526+
1527+
.settings-row-values {
1528+
margin-left: 4px;
1529+
}
1530+
1531+
.settings-row-label {
1532+
font-size: 13px;
1533+
color: var(--color-base-content);
1534+
margin-right: 16px;
1535+
}
1536+
1537+
.settings-section-title {
1538+
font-size: 12px;
1539+
font-weight: 600;
1540+
text-transform: uppercase;
1541+
letter-spacing: 0.04em;
1542+
color: color-mix(in srgb, var(--color-base-content) 60%, transparent);
1543+
margin: 12px 4px 4px 4px;
1544+
}
1545+
1546+
#settingsDialog .zoomButton {
1547+
border: none;
1548+
width: 32px;
1549+
height: 32px;
1550+
margin: 0 2px;
1551+
}
1552+
1553+
#settingsDialog .resetButton {
1554+
border: none;
1555+
height: 32px;
1556+
margin-left: 8px;
1557+
padding: 0 12px;
15631558
}
15641559

15651560
.arrangementViewer .minimap {

0 commit comments

Comments
 (0)