diff --git a/assets/Bravura.zip b/assets/Bravura.zip
new file mode 100644
index 00000000..e7c4ab08
Binary files /dev/null and b/assets/Bravura.zip differ
diff --git a/assets/Hufnagel.zip b/assets/Hufnagel.zip
new file mode 100644
index 00000000..25f162cd
Binary files /dev/null and b/assets/Hufnagel.zip differ
diff --git a/assets/fonts/Bravura.woff2 b/assets/fonts/Bravura.woff2
new file mode 100644
index 00000000..2ff46681
Binary files /dev/null and b/assets/fonts/Bravura.woff2 differ
diff --git a/assets/fonts/Bravura.xml b/assets/fonts/Bravura.xml
new file mode 100644
index 00000000..5eff2763
--- /dev/null
+++ b/assets/fonts/Bravura.xml
@@ -0,0 +1,1311 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/assets/fonts/Hufnagel.woff2 b/assets/fonts/Hufnagel.woff2
new file mode 100644
index 00000000..42d5ea14
Binary files /dev/null and b/assets/fonts/Hufnagel.woff2 differ
diff --git a/assets/fonts/Hufnagel.xml b/assets/fonts/Hufnagel.xml
new file mode 100644
index 00000000..70c3afc6
--- /dev/null
+++ b/assets/fonts/Hufnagel.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/assets/img/hufnagel/climacus.png b/assets/img/hufnagel/climacus.png
new file mode 100644
index 00000000..c2612d93
Binary files /dev/null and b/assets/img/hufnagel/climacus.png differ
diff --git a/assets/img/hufnagel/clivis.png b/assets/img/hufnagel/clivis.png
new file mode 100644
index 00000000..0eaa3e37
Binary files /dev/null and b/assets/img/hufnagel/clivis.png differ
diff --git a/assets/img/hufnagel/distropha.png b/assets/img/hufnagel/distropha.png
new file mode 100644
index 00000000..3cda313f
Binary files /dev/null and b/assets/img/hufnagel/distropha.png differ
diff --git a/assets/img/hufnagel/pes.png b/assets/img/hufnagel/pes.png
new file mode 100644
index 00000000..30a78f83
Binary files /dev/null and b/assets/img/hufnagel/pes.png differ
diff --git a/assets/img/hufnagel/porrectus.png b/assets/img/hufnagel/porrectus.png
new file mode 100644
index 00000000..3e737833
Binary files /dev/null and b/assets/img/hufnagel/porrectus.png differ
diff --git a/assets/img/hufnagel/pressus.png b/assets/img/hufnagel/pressus.png
new file mode 100644
index 00000000..472a4906
Binary files /dev/null and b/assets/img/hufnagel/pressus.png differ
diff --git a/assets/img/hufnagel/scandicus.png b/assets/img/hufnagel/scandicus.png
new file mode 100644
index 00000000..ea999218
Binary files /dev/null and b/assets/img/hufnagel/scandicus.png differ
diff --git a/assets/img/hufnagel/torculus.png b/assets/img/hufnagel/torculus.png
new file mode 100644
index 00000000..b376f220
Binary files /dev/null and b/assets/img/hufnagel/torculus.png differ
diff --git a/assets/style.css b/assets/style.css
index 02aef2d0..6c11a030 100644
--- a/assets/style.css
+++ b/assets/style.css
@@ -6,6 +6,20 @@
@import url('https://unpkg.com/diva.js@6.0.2/build/diva.css');
+@font-face {
+ font-family: 'Bravura';
+ src: url('./fonts/Bravura.woff2') format('woff2');
+ font-style: normal;
+ font-weight: normal;
+}
+
+@font-face {
+ font-family: 'Hufnagel';
+ src: url('./fonts/Hufnagel.woff2') format('woff2');
+ font-style: normal;
+ font-weight: normal;
+}
+
/* ----------------------- */
/* General styles
/* ----------------------- */
@@ -807,6 +821,23 @@ input[type='checkbox'].debug-mode-toggle:checked + label:before {
width: 42px;
height: 42px;
}
+.glyph-icon {
+ align-items: center;
+ display: flex;
+ height: 24px;
+ justify-content: center;
+ line-height: 0;
+ pointer-events: none;
+ width: 24px;
+}
+.glyph-icon svg {
+ display: block;
+ fill: currentColor;
+ height: 100%;
+ overflow: visible;
+ pointer-events: none;
+ width: 100%;
+}
.longel {
width: 100px;
height: 42px;
diff --git a/deployment/server/css/dashboard.css b/deployment/server/css/dashboard.css
index c526567f..20fea773 100644
--- a/deployment/server/css/dashboard.css
+++ b/deployment/server/css/dashboard.css
@@ -617,7 +617,7 @@ input.filename-input, select.file-input, textarea.file-input {
}
.folio_list {
- height: 80%;
+ height: 65%;
width: 100%;
overflow: scroll;
}
@@ -647,6 +647,39 @@ input.filename-input, select.file-input, textarea.file-input {
opacity: 0.4;
}
+#notation_type_container {
+ height: 15%;
+ width: 100%;
+ border-top: 1px solid #dfdfdf;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+}
+
+/* .file-container-title normally sizes to 10% of a much taller panel;
+ inside this short container that shrinks it below its own text height,
+ so it visually overlaps the radio row underneath. Override to auto. */
+#notation_type_container .file-container-title {
+ height: auto;
+ border-bottom: none;
+ margin-bottom: 4px;
+}
+
+.notation_type_options {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.notation_type_option {
+ font-size: 12px;
+ cursor: pointer;
+ display: inline-flex;
+ align-items: center;
+ margin: 0 6px;
+}
+
#upload_button_container {
float: right;
height: 10%;
diff --git a/package.json b/package.json
index a7acae1c..d2f1fb37 100644
--- a/package.json
+++ b/package.json
@@ -54,7 +54,7 @@
},
"dependencies": {
"d3": "^5.11.0",
- "diva.js": "github:DDMAL/diva.js#master",
+ "diva.js": "github:DDMAL/diva.js#5f530483f653db68af1cbb92bc8cc967b51763c3",
"http-server": "^14.1.1",
"jsonschema": "^1.2.4",
"pouchdb": "^7.1.1",
diff --git a/src/Dashboard/DashboardContent.ts b/src/Dashboard/DashboardContent.ts
index 9299f81d..0376dba8 100644
--- a/src/Dashboard/DashboardContent.ts
+++ b/src/Dashboard/DashboardContent.ts
@@ -56,6 +56,20 @@ export const uploadAreaHTML = `
+
+
Upload
diff --git a/src/Dashboard/Storage.ts b/src/Dashboard/Storage.ts
index 3b2053fb..6e017de7 100644
--- a/src/Dashboard/Storage.ts
+++ b/src/Dashboard/Storage.ts
@@ -2,6 +2,7 @@ import { v4 as uuidv4 } from 'uuid';
import PouchDB from 'pouchdb';
import { AllDocs, Doc, uploadsInfo } from '../Types';
import * as localManifest from '../../assets/manifest.json';
+import { restoreHufnagelForStorage } from '../utils/ConvertMei';
export const db = new PouchDB('Neon-User-Storage');
@@ -31,6 +32,7 @@ export function createManifest(
title: string,
mei: File,
bg: File,
+ notationType: string,
): Promise {
return new Promise(async (resolve) => {
const manifest = JSON.parse(JSON.stringify(localManifest));
@@ -38,12 +40,12 @@ export function createManifest(
manifest['title'] = title;
manifest['timestamp'] = new Date().toISOString();
- const meiPromise = new Promise((resolve) => {
+ const meiTextPromise = new Promise((resolve) => {
const meiReader = new FileReader();
meiReader.addEventListener('load', () => {
- resolve(meiReader.result);
+ resolve(meiReader.result as string);
});
- meiReader.readAsDataURL(mei);
+ meiReader.readAsText(mei);
});
const bgPromise = new Promise((resolve) => {
@@ -54,9 +56,22 @@ export function createManifest(
bgReader.readAsDataURL(bg);
});
- const meiUri = await meiPromise;
+ const meiText = await meiTextPromise;
+ const meiUri =
+ 'data:application/mei+xml;base64,' +
+ window.btoa(restoreHufnagelForStorage(meiText, notationType));
const bgUri = await bgPromise;
+ // Pre-seed this folio's LocalSettings entry (keyed by id, same format
+ // LocalSettings itself writes) so the Editing page's notation type
+ // dropdown reflects this choice on first open. The Editing page never
+ // reads @notationtype back out of the MEI to initialize the dropdown -
+ // it relies solely on this per-folio localStorage entry - and by the
+ // time it's loaded there, the MEI's real notationtype has already been
+ // stripped to a generic value for Verovio (see ConvertMei.ts), so it
+ // can't be recovered from the MEI at that point either.
+ window.localStorage.setItem(id, JSON.stringify({ notationType }));
+
manifest['image'] = bgUri;
manifest['mei_annotations'] = [
{
diff --git a/src/Dashboard/UploadArea.ts b/src/Dashboard/UploadArea.ts
index e564c6f1..c26fa1fc 100644
--- a/src/Dashboard/UploadArea.ts
+++ b/src/Dashboard/UploadArea.ts
@@ -15,7 +15,12 @@ async function handleUploadUpdate(
const spinner = document.querySelector('#uploading_spinner');
spinner.classList.add('visible');
- handleUploadAllDocuments(currentFolder)
+ const selectedNotationType: HTMLInputElement = document.querySelector(
+ 'input[name="upload_notation_type"]:checked',
+ );
+ const notationType = selectedNotationType?.value ?? 'square';
+
+ handleUploadAllDocuments(currentFolder, notationType)
.then((results) => {
setTimeout(async () => {
await updateDashboard();
diff --git a/src/Dashboard/UploadTools.ts b/src/Dashboard/UploadTools.ts
index f5f5829d..28502390 100644
--- a/src/Dashboard/UploadTools.ts
+++ b/src/Dashboard/UploadTools.ts
@@ -178,12 +178,13 @@ function createPairedFolio(
export async function handleUploadAllDocuments(
currentFolder: IFolder,
+ notationType: string,
): Promise<{ status: string; value?: string; reason?: any }[]> {
const folioPromises = fm
.getFolios()
.map(async ([name, mei, image]: [string, File, File]) => {
const id = uuidv4();
- return await uploadFolio(id, name, mei, image, currentFolder);
+ return await uploadFolio(id, name, mei, image, currentFolder, notationType);
});
const manuscriptPromises = []; //fm.getManuscripts()
@@ -208,13 +209,14 @@ async function uploadFolio(
mei: File,
image: File,
currentFolder: IFolder,
+ notationType: string,
): Promise {
const newName = fnConflictHandler(
name,
FileSystemTools.getAllNames(currentFolder),
);
return (
- createManifest(id, newName, mei, image)
+ createManifest(id, newName, mei, image, notationType)
.then((manifest) => {
const manifestBlob = new Blob([JSON.stringify(manifest, null, 2)], {
type: 'application/ld+json',
diff --git a/src/DisplayPanel/DisplayControls.ts b/src/DisplayPanel/DisplayControls.ts
index 50791b34..c3d9e79b 100644
--- a/src/DisplayPanel/DisplayControls.ts
+++ b/src/DisplayPanel/DisplayControls.ts
@@ -491,6 +491,48 @@ function setBurgerControls(): void {
});
}
+/**
+ * Set click listeners for Notation Type dropdown (Square / Hufnagel).
+ * Update the stored setting and notify the active view when the type changes.
+ */
+export function setNotationTypeControls(): void {
+ const dropdown = document.getElementById('notation-type-dropdown');
+ const label = document.getElementById('notation-type-label');
+
+ function applyNotationType(type: string): void {
+ label.textContent = `\xA0- ${type[0].toUpperCase() + type.slice(1)}`;
+ setSettings({ notationType: type });
+ document.dispatchEvent(
+ new CustomEvent('notationtypechange', { detail: { type } }),
+ );
+ }
+
+ function notationTypeClickaway(): void {
+ document.body.removeEventListener('click', notationTypeClickaway);
+ dropdown.classList.remove('is-active');
+ }
+
+ document.getElementById('notation-type-button').addEventListener('click', (evt) => {
+ evt.stopPropagation();
+ dropdown.classList.toggle('is-active');
+ if (dropdown.classList.contains('is-active')) {
+ document.body.addEventListener('click', notationTypeClickaway);
+ } else {
+ document.body.removeEventListener('click', notationTypeClickaway);
+ }
+ });
+
+ ['square', 'hufnagel'].forEach((type) => {
+ document.getElementById(`notation-type-${type}`).addEventListener('click', () => {
+ dropdown.classList.remove('is-active');
+ document.body.removeEventListener('click', notationTypeClickaway);
+ applyNotationType(type);
+ });
+ });
+
+ applyNotationType(getSettings().notationType ?? 'square');
+}
+
/**
* Set listener for "Display All" button in Display panel.
*/
@@ -583,6 +625,7 @@ export function initDisplayControls(
setHighlightKeyControls();
setDisplayAllListener();
loadHighlightSettings();
+ setNotationTypeControls();
const displayContents = document.getElementById('displayContents');
const toggleDisplay = document.getElementById('toggleDisplay');
diff --git a/src/DisplayPanel/DisplayPanel.ts b/src/DisplayPanel/DisplayPanel.ts
index aa6096c2..6470766e 100644
--- a/src/DisplayPanel/DisplayPanel.ts
+++ b/src/DisplayPanel/DisplayPanel.ts
@@ -135,7 +135,30 @@ function displayControlsPanel(handleZoom: ZoomHandler): string {
-
+
+
+
Notation Type:
+
+
+
+
+
+
+
+
`;
return contents;
diff --git a/src/DivaView.ts b/src/DivaView.ts
index 6e76f0d2..992818cc 100644
--- a/src/DivaView.ts
+++ b/src/DivaView.ts
@@ -33,6 +33,9 @@ class DivaView implements ViewInterface {
manifest: string,
) {
this.neonView = neonView;
+ document.addEventListener('notationtypechange', (evt: CustomEvent) => {
+ this.neonView.setNotationType(evt.detail.type);
+ });
this.updateCallbacks = [];
this.divaReady = false;
this.diva = new Diva('container', {
diff --git a/src/NeonCore.ts b/src/NeonCore.ts
index 6a6285ff..94fdbea7 100644
--- a/src/NeonCore.ts
+++ b/src/NeonCore.ts
@@ -2,6 +2,8 @@ import {
checkOutOfBoundsGlyphs,
convertToVerovio,
removeColumnLabel,
+ restoreHufnagelForStorage,
+ stripHufnagelForVerovio,
} from './utils/ConvertMei';
import * as Validation from './Validation';
import VerovioWrapper from './VerovioWrapper';
@@ -268,6 +270,11 @@ class NeonCore {
*/
loadData(pageURI: string, data: string, dirty = false): Promise {
Validation.sendForValidation(removeColumnLabel(data));
+ // TEMPORARY Verovio compatibility shim (see ConvertMei.ts) - every path
+ // into the worker funnels through here, so this is the one place that
+ // unconditionally keeps notationtype="neume.hufnagel"/"neume.square"
+ // and @con from ever reaching Verovio.
+ data = stripHufnagelForVerovio(data);
this.lastPageLoaded = pageURI;
/* A promise is returned that will resolve to the result of the action.
* However the value that is must return comes from the Web Worker and
@@ -304,6 +311,25 @@ class NeonCore {
});
}
+ setNotationFont(type: string): Promise {
+ return new Promise((resolve): void => {
+ const message: VerovioMessage = {
+ id: uuidv4(),
+ action: 'setFont',
+ fontType: type,
+ };
+ function handle(evt: MessageEvent): void {
+ if (evt.data.id === message.id) {
+ evt.target.removeEventListener('message', handle);
+ this.lastPageLoaded = '';
+ resolve();
+ }
+ }
+ this.verovioWrapper.addEventListener('message', handle.bind(this));
+ this.verovioWrapper.postMessage(message);
+ });
+ }
+
/**
* Get the SVG for a specific page.
* @param pageURI - The URI of the selected page.
@@ -556,7 +582,7 @@ class NeonCore {
* This is based on the data stored in the cache. To save time,
* only entries marked as dirty will be updated.
*/
- async updateDatabase(): Promise {
+ async updateDatabase(notationType?: string): Promise {
type Doc = PouchDB.Core.GetMeta &
PouchDB.Core.IdMeta & { body: string; timestamp: string };
let updateTimestamp = false;
@@ -568,6 +594,14 @@ class NeonCore {
const index = this.annotations.findIndex((elem) => {
return elem.target === key;
});
+ // TEMPORARY Verovio compatibility shim (see ConvertMei.ts) - restore
+ // the canonical notationtype/@con encoding before persisting outside
+ // of Verovio's control. Does not mutate value.mei: the live cache
+ // entry must stay in Verovio-safe form for subsequent reloads/edits.
+ const meiForStorage = restoreHufnagelForStorage(
+ value.mei,
+ notationType,
+ );
// try to update server with PUT request (if applicable)
// this is simpler than expecting a specific API on the server
// and using POST requests, although that would be better if there
@@ -580,23 +614,25 @@ class NeonCore {
.fetch(this.annotations[index].body, {
method: 'PUT',
headers: { 'Content-Type': 'application/mei+xml' },
- body: value.mei,
+ body: meiForStorage,
})
.then((response) => {
if (response.ok) {
uri = this.annotations[index].body;
} else {
uri =
- 'data:application/mei+xml;base64,' + window.btoa(value.mei);
+ 'data:application/mei+xml;base64,' +
+ window.btoa(meiForStorage);
}
})
.catch((err) => {
console.error(err);
console.warn('Falling back to data URI');
- uri = 'data:application/mei+xml;base64,' + window.btoa(value.mei);
+ uri =
+ 'data:application/mei+xml;base64,' + window.btoa(meiForStorage);
});
} else {
- uri = 'data:application/mei+xml;base64,' + window.btoa(value.mei);
+ uri = 'data:application/mei+xml;base64,' + window.btoa(meiForStorage);
}
// Update URI in annotations, database
this.annotations[index].body = uri;
diff --git a/src/NeonView.ts b/src/NeonView.ts
index 6688b8f8..7dccd591 100644
--- a/src/NeonView.ts
+++ b/src/NeonView.ts
@@ -72,15 +72,15 @@ class NeonView {
this.NeumeEdit = new params.NeumeEdit(this);
this.textView.updateBBoxVisibility();
const { userMode, insertMode, selectionMode } = getSettings();
- document.getElementById(insertMode).classList.add('is-active');
- document.getElementById(selectionMode).classList.add('is-active');
+ document.getElementById(insertMode)?.classList.add('is-active');
+ document.getElementById(selectionMode)?.classList.add('is-active');
switch (userMode) {
case 'insert':
- document.getElementById(insertMode).click();
+ document.getElementById(insertMode)?.click();
break;
case 'edit':
default:
- document.getElementById(selectionMode).click();
+ document.getElementById(selectionMode)?.click();
break;
}
}
@@ -187,7 +187,7 @@ class NeonView {
export(): Promise {
return new Promise((resolve, reject): void => {
this.core
- .updateDatabase()
+ .updateDatabase(getSettings().notationType)
.then(() => {
this.manifest['mei_annotations'] = this.core.getAnnotations();
this.manifest.timestamp = new Date().toISOString();
@@ -212,7 +212,7 @@ class NeonView {
*/
save(): Promise {
setSavedStatus(true);
- return this.core.updateDatabase();
+ return this.core.updateDatabase(getSettings().notationType);
}
/**
@@ -240,6 +240,12 @@ class NeonView {
});
}
+ setNotationType(type: string): void {
+ this.core.setNotationFont(type).then(() => {
+ this.updateForCurrentPage();
+ });
+ }
+
/**
* Get the page's MEI file as a string.
* @param pageNo - The identifying URI of the page.
diff --git a/src/SingleView/SingleView.ts b/src/SingleView/SingleView.ts
index d9af3ea0..362f8600 100644
--- a/src/SingleView/SingleView.ts
+++ b/src/SingleView/SingleView.ts
@@ -31,6 +31,9 @@ class SingleView implements ViewInterface {
*/
constructor(neonView: NeonView, panel: DisplayConstructable, image: string) {
this.neonView = neonView;
+ document.addEventListener('notationtypechange', (evt: CustomEvent) => {
+ this.neonView.setNotationType(evt.detail.type);
+ });
this.container = document.getElementById('container');
this.updateCallbacks = new Array(0);
// Group will contain the image and the rendered SVG
diff --git a/src/SquareEdit/Contents.ts b/src/SquareEdit/Contents.ts
index 026ff3d6..8f55a507 100644
--- a/src/SquareEdit/Contents.ts
+++ b/src/SquareEdit/Contents.ts
@@ -1,3 +1,95 @@
+import { getFontData, buildGlyphIcon } from '../utils/GlyphIcons';
+
+export interface PrimitiveEntry {
+ id: string;
+ codepoint: string;
+ title: string;
+ iconDy?: number;
+ pngFallback?: string;
+}
+
+export const SQUARE_PRIMITIVES: PrimitiveEntry[] = [
+ { id: 'punctum', codepoint: 'E990', title: 'punctum' },
+ { id: 'virga', codepoint: 'E996', title: 'virga', iconDy: -9 },
+ { id: 'virgaReversed', codepoint: 'E997', title: 'Reversed Virga', iconDy: -9, pngFallback: 'virga_reversed' },
+ { id: 'diamond', codepoint: 'E991', title: 'inclinatum' },
+ { id: 'custos', codepoint: 'EA06', title: 'custos', iconDy: 9 },
+ { id: 'cClef', codepoint: 'E906', title: 'C Clef' },
+ { id: 'fClef', codepoint: 'E902', title: 'F Clef' },
+ { id: 'gClef', codepoint: 'E900', title: 'G Clef' },
+ { id: 'liquescentA', codepoint: 'E994', title: 'Liquescent A' },
+ { id: 'liquescentC', codepoint: 'E995', title: 'Liquescent C' },
+ { id: 'quilisma', codepoint: 'E99B', title: 'quilisma' },
+ { id: 'oriscus', codepoint: 'E99E', title: 'oriscus' },
+ { id: 'flat', codepoint: 'E260', title: 'Flat', iconDy: 9, pngFallback: 'accidFlat' },
+ { id: 'natural', codepoint: 'E261', title: 'Natural', pngFallback: 'accidNatural' },
+ { id: 'divLineMaxima', codepoint: 'E8F5', title: 'DivLine Maxima', pngFallback: 'divisio' },
+];
+
+export const HUFNAGEL_PRIMITIVES: PrimitiveEntry[] = [
+ { id: 'punctum', codepoint: 'E990', title: 'punctum' },
+ { id: 'virga', codepoint: 'E996', title: 'virga', iconDy: -13 },
+ { id: 'virgaReversed', codepoint: 'E997', title: 'UDV', iconDy: 13 },
+ { id: 'distropha', codepoint: 'E99F', title: 'Distropha', pngFallback: 'hufnagel/distropha' },
+ { id: 'custos', codepoint: 'EA06', title: 'custos', iconDy: 6 },
+ { id: 'cClef', codepoint: 'E906', title: 'C Clef' },
+ { id: 'fClef', codepoint: 'E902', title: 'F Clef' },
+ { id: 'liquescentA', codepoint: 'E994', title: 'Liquescent up' },
+ { id: 'flat', codepoint: 'E260', title: 'Flat', iconDy: 9, pngFallback: 'accidFlat' },
+ { id: 'natural', codepoint: 'E261', title: 'Natural', pngFallback: 'accidNatural' },
+ { id: 'divLineMaxima', codepoint: 'E8F5', title: 'DivLine Maxima', pngFallback: 'divisio' },
+];
+
+export async function buildPrimitiveTabHtml(notationType: string): Promise {
+ const isHufnagel = notationType === 'hufnagel';
+ const fontName = isHufnagel ? 'Hufnagel' : 'Bravura';
+ const primitives = isHufnagel ? HUFNAGEL_PRIMITIVES : SQUARE_PRIMITIVES;
+
+ const fontData = await getFontData(
+ fontName,
+ `${__ASSET_PREFIX__}assets/fonts/${fontName}.xml`,
+ );
+ const fallbackFontData = isHufnagel
+ ? await getFontData(
+ 'Bravura',
+ `${__ASSET_PREFIX__}assets/fonts/Bravura.xml`,
+ )
+ : null;
+
+ return primitives.map(({ id, codepoint, title, iconDy, pngFallback }) => {
+ const bbox = fontData.bboxMap.get(codepoint);
+ const fallbackBBox = fallbackFontData?.bboxMap.get(codepoint);
+ const fallbackPng = pngFallback ?? id;
+ const iconStyle = iconDy ? ` style="transform: translateY(${iconDy}px)"` : '';
+ // If the primary font lacks this glyph and there's a font-specific PNG override, use that
+ // before falling back to Bravura (which would show a Square notation glyph).
+ const useOverridePng = !bbox && pngFallback?.startsWith('hufnagel/');
+ const icon = !useOverridePng && (bbox || fallbackBBox)
+ ? `${buildGlyphIcon(
+ codepoint,
+ bbox ? fontData.fontName : fallbackFontData.fontName,
+ bbox ?? fallbackBBox,
+ )}`
+ : `
`;
+ return `
+
+
`;
+ }).join('\n');
+}
+
+const GROUPINGS = ['pes', 'clivis', 'scandicus', 'climacus', 'torculus', 'porrectus', 'pressus'] as const;
+
+export function buildGroupingTabHtml(notationType: string): string {
+ const imgDir = notationType === 'hufnagel'
+ ? `${__ASSET_PREFIX__}assets/img/hufnagel/`
+ : `${__ASSET_PREFIX__}assets/img/`;
+ return GROUPINGS.map(id =>
+ `
+
+
`
+ ).join('\n');
+}
+
/**
* HTML for each insert tab (neume, grouping, clef, system, and division).
*/
@@ -47,27 +139,7 @@ export const insertTabHtml: Record = {
`,
- groupingTab: `
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
`,
+ groupingTab: '', // unused — call buildGroupingTabHtml(notationType) instead
systemTab: `
diff --git a/src/SquareEdit/Controls.ts b/src/SquareEdit/Controls.ts
index e380656e..c9da05e8 100644
--- a/src/SquareEdit/Controls.ts
+++ b/src/SquareEdit/Controls.ts
@@ -36,21 +36,82 @@ export function bindInsertTabs(insertHandler: InsertHandler): void {
deactivate('.insertTab');
activate(tab, insertHandler);
setSettings({ insertTab: tab as InsertTabType });
- document.getElementById('insert_data').innerHTML =
- Contents.insertTabHtml[tab];
- bindElements(insertHandler);
- deactivate('.insertel');
- try {
- const { insertMode } = getSettings();
- activate(insertMode, insertHandler);
- } catch (e) {
- const firstOption = document.getElementsByClassName('insertel')[0];
- activate(firstOption.id, insertHandler);
+ if (tab === 'primitiveTab') {
+ const { notationType } = getSettings();
+ setPrimitiveFallback(insertHandler);
+ renderPrimitiveTab(notationType ?? 'square', insertHandler);
+ } else if (tab === 'groupingTab') {
+ const { notationType } = getSettings();
+ document.getElementById('insert_data').innerHTML =
+ Contents.buildGroupingTabHtml(notationType ?? 'square');
+ bindElements(insertHandler);
+ deactivate('.insertel');
+ activateCurrentOrFirst(insertHandler);
+ } else {
+ document.getElementById('insert_data').innerHTML =
+ Contents.insertTabHtml[tab];
+ bindElements(insertHandler);
+ deactivate('.insertel');
+ activateCurrentOrFirst(insertHandler);
}
});
});
}
+export async function updatePrimitiveTab(notationType: string, insertHandler: InsertHandler): Promise {
+ const insertData = document.getElementById('insert_data');
+ if (!insertData) return;
+ const { insertTab } = getSettings();
+ if (insertTab !== 'primitiveTab') return;
+ setPrimitiveFallback(insertHandler);
+ await renderPrimitiveTab(notationType, insertHandler);
+}
+
+export function updateGroupingTab(notationType: string, insertHandler: InsertHandler): void {
+ const insertData = document.getElementById('insert_data');
+ if (!insertData) return;
+ const { insertTab } = getSettings();
+ if (insertTab !== 'groupingTab') return;
+ insertData.innerHTML = Contents.buildGroupingTabHtml(notationType);
+ bindElements(insertHandler);
+ deactivate('.insertel');
+ activateCurrentOrFirst(insertHandler);
+}
+
+function setPrimitiveFallback(insertHandler: InsertHandler): void {
+ const insertData = document.getElementById('insert_data');
+ if (!insertData) return;
+ insertData.innerHTML = Contents.insertTabHtml['primitiveTab'];
+ bindElements(insertHandler);
+ deactivate('.insertel');
+ activateCurrentOrFirst(insertHandler);
+}
+
+async function renderPrimitiveTab(notationType: string, insertHandler: InsertHandler): Promise {
+ try {
+ const html = await Contents.buildPrimitiveTabHtml(notationType);
+ const insertData = document.getElementById('insert_data');
+ if (!insertData) return;
+ const { insertTab } = getSettings();
+ if (insertTab !== 'primitiveTab') return;
+ insertData.innerHTML = html;
+ bindElements(insertHandler);
+ deactivate('.insertel');
+ activateCurrentOrFirst(insertHandler);
+ } catch (err) {
+ console.error('[GlyphIcons] Failed to load glyphs, using PNG fallback:', err);
+ }
+}
+
+function activateCurrentOrFirst(insertHandler: InsertHandler): void {
+ const { insertMode } = getSettings();
+ const currentOption = document.getElementById(insertMode);
+ const firstOption = document.getElementsByClassName('insertel')[0];
+ const option = currentOption ?? firstOption;
+ if (!option) return;
+ activate(option.id, insertHandler);
+}
+
/**
* Initialize Edit and Insert control panels.
*/
@@ -77,13 +138,14 @@ export function initInsertEditControls(): void {
setSettings({ userMode: 'insert' });
- (document.querySelector('.insertel.is-active')).click();
+ const activeInsert = document.querySelector('.insertel.is-active');
+ activeInsert?.click();
editPanel
.querySelector('.side-panel-btn.sel-by.is-active')
- .classList.add('unfocused');
+ ?.classList.add('unfocused');
insertPanel
.querySelector('.side-panel-btn.insertel.is-active')
- .classList.remove('unfocused');
+ ?.classList.remove('unfocused');
});
// event listener for when user clicks inside Edit panel
@@ -96,10 +158,10 @@ export function initInsertEditControls(): void {
insertPanel
.querySelector('.side-panel-btn.insertel.is-active')
- .classList.add('unfocused');
+ ?.classList.add('unfocused');
editPanel
.querySelector('.side-panel-btn.sel-by.is-active')
- .classList.remove('unfocused');
+ ?.classList.remove('unfocused');
});
insertHeading.addEventListener('click', (e) => {
@@ -171,8 +233,9 @@ export function initInsertEditControls(): void {
*/
function activate(id: string, insertHandler: InsertHandler): void {
const selectedTab = document.getElementById(id);
+ if (!selectedTab) return;
selectedTab.classList.add('is-active');
- if (document.getElementById(id).classList.contains('insertel')) {
+ if (selectedTab.classList.contains('insertel')) {
insertHandler.insertActive(id);
setSettings({ insertMode: id as InsertType });
}
diff --git a/src/SquareEdit/DivaEditMode.ts b/src/SquareEdit/DivaEditMode.ts
index e707661a..ee0f6cf2 100644
--- a/src/SquareEdit/DivaEditMode.ts
+++ b/src/SquareEdit/DivaEditMode.ts
@@ -2,6 +2,8 @@ import {
bindInsertTabs,
initInsertEditControls,
initSelectionButtons,
+ updatePrimitiveTab,
+ updateGroupingTab,
} from './Controls';
import * as Select from '../utils/Select';
import InsertHandler from './InsertHandler';
@@ -36,6 +38,11 @@ class DivaEdit implements NeumeEditInterface {
bindInsertTabs(this.insertHandler);
const { insertTab } = getSettings();
document.getElementById(insertTab).click();
+ document.addEventListener('notationtypechange', (evt: CustomEvent) => {
+ updatePrimitiveTab(evt.detail.type, this.insertHandler);
+ updateGroupingTab(evt.detail.type, this.insertHandler);
+ });
+ updatePrimitiveTab(getSettings().notationType ?? 'square', this.insertHandler);
Select.setSelectHelperObjects(this.neonView, this.dragHandler);
this.setSelectListeners();
SelectOptions.initNeonView(this.neonView);
diff --git a/src/SquareEdit/InsertHandler.ts b/src/SquareEdit/InsertHandler.ts
index 9f9d717f..f6ab4838 100644
--- a/src/SquareEdit/InsertHandler.ts
+++ b/src/SquareEdit/InsertHandler.ts
@@ -73,6 +73,7 @@ class InsertHandler {
case 'torculus':
case 'porrectus':
case 'pressus':
+ case 'distropha':
const contour = this.neonView.info.getContourByValue(
buttonId.charAt(0).toUpperCase() + buttonId.slice(1),
);
diff --git a/src/SquareEdit/SingleEditMode.ts b/src/SquareEdit/SingleEditMode.ts
index c2b13cd0..e2f744cc 100644
--- a/src/SquareEdit/SingleEditMode.ts
+++ b/src/SquareEdit/SingleEditMode.ts
@@ -2,6 +2,8 @@ import {
bindInsertTabs,
initInsertEditControls,
initSelectionButtons,
+ updatePrimitiveTab,
+ updateGroupingTab,
} from './Controls';
import { setHighlightOption } from '../DisplayPanel/DisplayControls';
import { initUndoRedoPanel } from '../utils/EditControls';
@@ -50,6 +52,11 @@ class SingleEditMode implements NeumeEditInterface {
bindInsertTabs(this.insertHandler);
const { insertTab } = getSettings();
document.getElementById(insertTab).click();
+ document.addEventListener('notationtypechange', (evt: CustomEvent) => {
+ updatePrimitiveTab(evt.detail.type, this.insertHandler);
+ updateGroupingTab(evt.detail.type, this.insertHandler);
+ });
+ updatePrimitiveTab(getSettings().notationType ?? 'square', this.insertHandler);
Select.setSelectHelperObjects(this.neonView, this.dragHandler);
this.setSelectListeners();
diff --git a/src/Types.ts b/src/Types.ts
index 6fb7f885..c93b25f8 100644
--- a/src/Types.ts
+++ b/src/Types.ts
@@ -272,6 +272,7 @@ export type VerovioMessage = {
mei?: string;
elementId?: string;
editorAction?: EditorAction;
+ fontType?: string;
};
export type VerovioResponse = {
@@ -394,6 +395,7 @@ export type InsertType =
| 'gClef'
| 'liquescentA'
| 'liquescentC'
+ | 'distropha'
| 'quilisma'
| 'orcisus'
| 'flat'
diff --git a/src/utils/ConvertMei.ts b/src/utils/ConvertMei.ts
index 1cc338e1..96dfaf39 100644
--- a/src/utils/ConvertMei.ts
+++ b/src/utils/ConvertMei.ts
@@ -24,7 +24,110 @@ function copyAttributes(src: Element, dst: Element): void {
}
}
-export function convertToNeon(staffBasedMei: string): string {
+// --- BEGIN TEMPORARY Verovio compatibility shim (see issue #1360) ---
+//
+// Verovio segfaults on notationtype="neume.hufnagel" on , and
+// notationtype="neume.square" has never been exercised either (every
+// existing file/fixture only uses bare "neume"). So Neon's canonical MEI
+// (what gets downloaded/saved) keeps the correct staffDef notationtype
+// ("neume.hufnagel" / "neume.square"), but whatever is actually handed to
+// the Verovio worker must always use bare notationtype="neume" (the
+// Hufnagel/Square look is forced via font substitution instead - see
+// setNotationFont / VerovioWorker-dev.js, which this shim does not touch).
+//
+// @con/@ligated on are NOT touched on the way INTO Verovio, for either
+// notation type. Square's connections are Square's own real @ligated
+// (needed for Verovio's ligature-merge glyph rendering - that mechanism is
+// real and unrelated to this shim). Hufnagel's connections are rendered
+// from each note's own individual glyph (e.g. virga + UDV are two separate
+// primitives, not a merged glyph) - @con is pure semantic metadata Verovio
+// does not need for rendering Hufnagel at all. An earlier version of this
+// shim auto-derived @ligated from @con before Verovio to try to force the
+// same merge-rendering Square uses, and that was simply wrong: it made
+// Verovio invoke the Square ligature-glyph codepath, which the Hufnagel
+// font substitution does not cover, corrupting the render (UDV disappeared,
+// its partner note misshapen). Removed for good - do not reintroduce.
+//
+// The one place @con/@ligated IS still touched: if Verovio's own
+// toggleLigature editor action is used on a Hufnagel document (reusing
+// Square's button, planned but not yet wired up) and leaves @ligated="true"
+// behind after live editing, that must be converted back to @con="e" before
+// download/save, since "ligature" must never appear in Hufnagel's canonical
+// MEI. This is a no-op for documents that never went through that action.
+//
+// This entire block can be deleted once Verovio natively supports
+// notationtype="neume.hufnagel"/"neume.square".
+const HUFNAGEL_NOTATIONTYPE = 'neume.hufnagel';
+const SQUARE_NOTATIONTYPE = 'neume.square';
+const HUFNAGEL_CON_VALUE = 'e';
+
+// Hufnagel only: reverse of Verovio's own toggleLigature editor action. For
+// each adjacent pair of @ligated s, remove @ligated from both and
+// restore @con="e" on the second one only.
+function convertLigatedToCon(mei: Element): void {
+ const ncs = Array.from(mei.getElementsByTagName('nc'));
+ let idx = 0;
+ while (idx < ncs.length) {
+ if (
+ ncs[idx].getAttribute('ligated') &&
+ idx + 1 < ncs.length &&
+ ncs[idx + 1].getAttribute('ligated')
+ ) {
+ ncs[idx].removeAttribute('ligated');
+ ncs[idx + 1].removeAttribute('ligated');
+ ncs[idx + 1].setAttribute('con', HUFNAGEL_CON_VALUE);
+ idx += 2;
+ } else {
+ idx += 1;
+ }
+ }
+}
+
+// String-in/string-out wrappers for callers (NeonCore.ts) that only have
+// the MEI as a string, not an already-parsed DOM.
+export function stripHufnagelForVerovio(meiString: string): string {
+ const parser = new DOMParser();
+ const meiDoc = parser.parseFromString(meiString, 'text/xml');
+ const staffDef = meiDoc.documentElement.querySelector('staffDef');
+ const notationType = staffDef?.getAttribute('notationtype');
+
+ if (
+ notationType === HUFNAGEL_NOTATIONTYPE ||
+ notationType === SQUARE_NOTATIONTYPE
+ ) {
+ staffDef.setAttribute('notationtype', 'neume');
+ }
+
+ return vkbeautify.xml(new XMLSerializer().serializeToString(meiDoc));
+}
+
+// Used by Save (NeonCore.updateDatabase) - this is Neon's own internal
+// working copy, which gets fed straight back into Verovio on next load, so
+// it must faithfully keep whatever Verovio's live in-memory document
+// actually has (including @ligated from a real toggleLigature action, and
+// whatever else that action changed, e.g. clearing @tilt) rather than
+// "fixing" it to the canonical @con-only form. Only @notationtype is
+// restored here, since Verovio itself never remembers that.
+export function restoreHufnagelForStorage(
+ meiString: string,
+ notationType?: string,
+): string {
+ const parser = new DOMParser();
+ const meiDoc = parser.parseFromString(meiString, 'text/xml');
+ if (notationType === 'hufnagel' || notationType === 'square') {
+ meiDoc.documentElement
+ .querySelector('staffDef')
+ ?.setAttribute('notationtype', `neume.${notationType}`);
+ }
+
+ return vkbeautify.xml(new XMLSerializer().serializeToString(meiDoc));
+}
+// --- END TEMPORARY Verovio compatibility shim ---
+
+export function convertToNeon(
+ staffBasedMei: string,
+ notationType?: string,
+): string {
const parser = new DOMParser();
const serializer = new XMLSerializer();
const meiDoc = parser.parseFromString(staffBasedMei, 'text/xml');
@@ -148,6 +251,16 @@ export function convertToNeon(staffBasedMei: string): string {
scoreDef.insertAdjacentElement('afterend', colLayout);
}
+ if (notationType === 'hufnagel' || notationType === 'square') {
+ mei
+ .querySelector('staffDef')
+ ?.setAttribute('notationtype', `neume.${notationType}`);
+ }
+
+ if (notationType === 'hufnagel') {
+ convertLigatedToCon(mei);
+ }
+
return vkbeautify.xml(serializer.serializeToString(meiDoc));
function calculateAndAddZone(
diff --git a/src/utils/EditControls.ts b/src/utils/EditControls.ts
index c052352b..39046a03 100644
--- a/src/utils/EditControls.ts
+++ b/src/utils/EditControls.ts
@@ -2,6 +2,7 @@ import * as Notification from './Notification';
import NeonView from '../NeonView';
import { convertToNeon } from './ConvertMei';
import { EditorAction } from '../Types';
+import { getSettings } from './LocalSettings';
/**
* Set top navbar event listeners.
@@ -49,7 +50,8 @@ export function initNavbar(neonView: NeonView): void {
const uri = neonView.view.getCurrentPageURI();
neonView.getPageMEI(uri).then((mei) => {
const data =
- 'data:application/mei+xml;base64,' + window.btoa(convertToNeon(mei));
+ 'data:application/mei+xml;base64,' +
+ window.btoa(convertToNeon(mei, getSettings().notationType));
document.getElementById('getmei').setAttribute('href', data);
document
.getElementById('getmei')
diff --git a/src/utils/GlyphIcons.ts b/src/utils/GlyphIcons.ts
new file mode 100644
index 00000000..c6952d23
--- /dev/null
+++ b/src/utils/GlyphIcons.ts
@@ -0,0 +1,58 @@
+interface BBox { x: number; y: number; w: number; h: number; }
+
+interface FontData {
+ fontName: string;
+ bboxMap: Map;
+}
+
+const ICON_SIZE = 24;
+
+const cache = new Map>();
+
+async function loadFontData(
+ fontName: string,
+ bboxUrl: string,
+): Promise {
+ const response = await fetch(bboxUrl);
+ if (!response.ok) throw new Error(`Unable to load ${bboxUrl}`);
+ const xml = await response.text();
+ const bboxMap = new Map();
+ const parser = new DOMParser();
+ const doc = parser.parseFromString(xml, 'text/xml');
+ doc.querySelectorAll('g[c]').forEach(el => {
+ const c = el.getAttribute('c');
+ const x = parseFloat(el.getAttribute('x') ?? '0');
+ const y = parseFloat(el.getAttribute('y') ?? '0');
+ const w = parseFloat(el.getAttribute('w') ?? '0');
+ const h = parseFloat(el.getAttribute('h') ?? '0');
+ if (c && w > 0 && h > 0) bboxMap.set(c, { x, y, w, h });
+ });
+
+ await document.fonts.load(`1000px "${fontName}"`);
+
+ return { fontName, bboxMap };
+}
+
+export function getFontData(
+ fontName: string,
+ bboxUrl: string,
+): Promise {
+ if (!cache.has(fontName)) {
+ cache.set(fontName, loadFontData(fontName, bboxUrl));
+ }
+ return cache.get(fontName)!;
+}
+
+export function buildGlyphIcon(codepoint: string, fontName: string, bbox: BBox): string {
+ const pad = bbox.h * 0.08;
+ const vx = bbox.x - pad;
+ const vy = bbox.y - pad;
+ const vw = bbox.w + pad * 2;
+ const vh = bbox.h + pad * 2;
+ const cp = parseInt(codepoint, 16);
+ const char = String.fromCodePoint(cp);
+ // font-size 1000 matches units-per-em=1000, so bbox coords directly frame the glyph
+ return ``;
+}
diff --git a/src/utils/LocalSettings.ts b/src/utils/LocalSettings.ts
index d4be1d2c..700881e4 100644
--- a/src/utils/LocalSettings.ts
+++ b/src/utils/LocalSettings.ts
@@ -31,6 +31,7 @@ export interface Settings {
userMode: UserType;
insertMode: InsertType;
insertTab: InsertTabType;
+ notationType: string;
selectionMode: SelectionType;
debugMode: boolean;
displayBBox: boolean;
@@ -56,6 +57,7 @@ const DEFAULT_SETTINGS: Settings = {
userMode: 'edit',
insertMode: 'punctum',
insertTab: 'primitiveTab',
+ notationType: 'square',
selectionMode: 'selBySyllable',
debugMode: false,
displayBBox: false,
diff --git a/src/workers/FontLoader.js b/src/workers/FontLoader.js
new file mode 100644
index 00000000..493d3672
--- /dev/null
+++ b/src/workers/FontLoader.js
@@ -0,0 +1,16 @@
+const fontBase64Promises = new Map();
+
+function fetchFontBase64(url) {
+ if (!fontBase64Promises.has(url)) {
+ const base64Promise = fetch(url)
+ .then(response => response.arrayBuffer())
+ .then(buffer => {
+ const bytes = new Uint8Array(buffer);
+ let binary = '';
+ for (let i = 0; i < bytes.byteLength; i++) binary += String.fromCharCode(bytes[i]);
+ return btoa(binary);
+ });
+ fontBase64Promises.set(url, base64Promise);
+ }
+ return fontBase64Promises.get(url);
+}
diff --git a/src/workers/VerovioWorker-dev.js b/src/workers/VerovioWorker-dev.js
index 8e6472ac..0e5aa207 100644
--- a/src/workers/VerovioWorker-dev.js
+++ b/src/workers/VerovioWorker-dev.js
@@ -1,4 +1,5 @@
importScripts('../assets/js/verovio-toolkit-wasm.js');
+importScripts('./FontLoader.js');
let toolkit;
const backlog = [];
@@ -7,7 +8,7 @@ const backlog = [];
* Parse and respond to messages sent by NeonCore.
* @param {MessageEvent} evt
*/
-function handleNeonEvent(evt) {
+async function handleNeonEvent(evt) {
const data = evt.data;
const result = {
id: data.id,
@@ -35,6 +36,33 @@ function handleNeonEvent(evt) {
case 'renderToSVG':
result.svg = toolkit.renderToSVG(1);
break;
+ case 'setFont': {
+ const baseOptions = {
+ inputFrom: 'mei',
+ footer: 'none',
+ header: 'none',
+ pageMarginLeft: 0,
+ pageMarginTop: 0,
+ useFacsimile: false,
+ svgAdditionalAttribute: ['syllable@precedes', 'syllable@follows'],
+ svgCss: 'g.nc, g.custos, g.clef, g.accid, g.divLine {stroke: currentColor; stroke-width: 30px;}',
+ };
+ try {
+ if (data.fontType === 'hufnagel') {
+ const b64 = await fetchFontBase64('../assets/Hufnagel.zip');
+ console.log('[VerovioWorker] b64 length:', b64.length);
+ toolkit.setOptions({ ...baseOptions, font: 'Hufnagel', fontAddCustom: [b64], fontFallback: 'Bravura' });
+ console.log('[VerovioWorker] setOptions Hufnagel done');
+ } else {
+ toolkit.setOptions({ ...baseOptions, font: 'Bravura', fontAddCustom: '' });
+ console.log('[VerovioWorker] setOptions Bravura done');
+ }
+ console.log('[VerovioWorker] setFont done:', data.fontType);
+ } catch (e) {
+ console.error('[VerovioWorker] setFont ERROR:', e);
+ }
+ break;
+ }
default:
break;
}
diff --git a/src/workers/VerovioWorker.js b/src/workers/VerovioWorker.js
index f8c63b3e..03446ba0 100644
--- a/src/workers/VerovioWorker.js
+++ b/src/workers/VerovioWorker.js
@@ -1,3 +1,5 @@
+importScripts('./FontLoader.js');
+
let toolkit;
const backlog = [];
@@ -5,7 +7,7 @@ const backlog = [];
* Parse and respond to messages sent by NeonCore.
* @param {MessageEvent} evt
*/
-function handleNeonEvent(evt) {
+async function handleNeonEvent(evt) {
const data = evt.data;
const result = {
id: data.id,
@@ -33,6 +35,33 @@ function handleNeonEvent(evt) {
case 'renderToSVG':
result.svg = toolkit.renderToSVG(1);
break;
+ case 'setFont': {
+ const baseOptions = {
+ inputFrom: 'mei',
+ footer: 'none',
+ header: 'none',
+ pageMarginLeft: 0,
+ pageMarginTop: 0,
+ useFacsimile: false,
+ svgAdditionalAttribute: ['syllable@precedes', 'syllable@follows'],
+ svgCss: 'g.nc, g.custos, g.clef, g.accid, g.divLine {stroke: currentColor; stroke-width: 30px;}',
+ };
+ try {
+ if (data.fontType === 'hufnagel') {
+ const b64 = await fetchFontBase64('../assets/Hufnagel.zip');
+ console.log('[VerovioWorker] b64 length:', b64.length);
+ toolkit.setOptions({ ...baseOptions, font: 'Hufnagel', fontAddCustom: [b64], fontFallback: 'Bravura' });
+ console.log('[VerovioWorker] setOptions Hufnagel done');
+ } else {
+ toolkit.setOptions({ ...baseOptions, font: 'Bravura', fontAddCustom: '' });
+ console.log('[VerovioWorker] setOptions Bravura done');
+ }
+ console.log('[VerovioWorker] setFont done:', data.fontType);
+ } catch (e) {
+ console.error('[VerovioWorker] setFont ERROR:', e);
+ }
+ break;
+ }
default:
break;
}
diff --git a/yarn.lock b/yarn.lock
index b9309f99..71aaf316 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3519,7 +3519,7 @@ diffie-hellman@^5.0.0:
miller-rabin "^4.0.0"
randombytes "^2.0.0"
-"diva.js@github:DDMAL/diva.js#master":
+"diva.js@github:DDMAL/diva.js#5f530483f653db68af1cbb92bc8cc967b51763c3":
version "6.0.2"
resolved "https://codeload.github.com/DDMAL/diva.js/tar.gz/5f530483f653db68af1cbb92bc8cc967b51763c3"
dependencies: