Skip to content

Port Hufnagel font switching to production Verovio worker#1376

Open
zih-syuan wants to merge 25 commits into
developfrom
zih-syuan/hufnagel-ui
Open

Port Hufnagel font switching to production Verovio worker#1376
zih-syuan wants to merge 25 commits into
developfrom
zih-syuan/hufnagel-ui

Conversation

@zih-syuan

Copy link
Copy Markdown

Related issues

Related to #1367 (font switching implemented; dropdown-freeze and switch-warning behavior still open for discussion, not closed by this PR)
Closes #1368

What this does

  • Ports the Hufnagel/Bravura(Square notation) font-switching logic (fetchHufnagelBase64, setFont case) from VerovioWorker-dev.js to the production worker VerovioWorker.js, so the feature actually works once live on neon.simssa.ca.
  • INSERT panel icons now render dynamically instead of using static PNGs.

Testing

  • yarn cypress:test: all passing (44 passing, 6 pending — unrelated to this change)
  • Manual local test: confirmed via browser DevTools Network/Console that VerovioWorker.js (not the dev version) loads, and Hufnagel font renders correctly

zih-syuan and others added 22 commits June 12, 2026 12:47
diva.js#master no longer exists; pin to last known good commit.
See #1365.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds assets/Hufnagel.zip for Verovio fontAddCustom integration.
Font source files live in zih-syuan/hufnagel-font; this branch
only tracks the packaged artifact.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a Square/Hufnagel selector below Display Options. Clicking updates
the label and logs to console; engine wiring (setFont / setNotationType)
is the next step.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- VerovioWorker: add setFont handler (lazy-loads Hufnagel.zip as base64,
  calls toolkit.setOptions with full options)
- NeonCore: add setNotationFont() — sends setFont to worker, clears
  lastPageLoaded to bypass cache on next render
- NeonView: add setNotationType() — calls setNotationFont then re-renders
- DisplayControls/DisplayPanel: thread neonView through setDisplayListeners
  → initDisplayControls → setNotationTypeControls so dropdown is wired
- Interfaces/SingleView/DivaView: update setDisplayListeners signature

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…fallback

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
E99F is absent from Hufnagel.zip so Bravura fallback showed a square
notation glyph. Generate a two-diamond icon via Verovio and use it as
pngFallback for the distropha button in Hufnagel mode.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…back

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Verovio segfaults on staffDef notationtype="neume.hufnagel" (and
"neume.square" is unverified) and doesn't render @con, so canonical
Hufnagel connections (@con="e") and the real notationtype must never
reach the Verovio worker directly. Add a temporary conversion shim in
ConvertMei.ts (stripHufnagelForVerovio / restoreHufnagelForStorage)
applied unconditionally at the two real boundaries - NeonCore.loadData()
before anything reaches Verovio, and NeonCore.updateDatabase()/save()/
export() before anything is persisted or downloaded - so every load
path (fresh fetch, cache hit, undo, redo) and every persistence path
(save, export, download) stays consistent. See issues #1360, #1366.

Co-Authored-By: Claude <noreply@anthropic.com>
Drop the hufnagel liquescent down primitive and the alternate F clef
variant (fClefHufnagel) from the Insert panel, leaving one F clef
button shared with square notation.
Nudge virga, virgaReversed, and custos icon offsets in the Insert
panel so they align visually with the rest of the hufnagel primitives.
…handling

- Upload modal now has a Notation Type choice, written into the MEI and
  synced to the Editing page dropdown.
- Removed the auto con->ligated conversion before Verovio; it was
  corrupting real content. @con now passes through untouched.
- Save no longer converts @ligated back to @con (only Download does),
  so a manually toggled connection survives a reload.

Co-Authored-By: Claude <noreply@anthropic.com>
@zih-syuan
zih-syuan requested a review from kyrieb-ekat July 7, 2026 17:03
@lpugin

lpugin commented Jul 8, 2026

Copy link
Copy Markdown
Member

Is this still using the Verovio version from RISM Digital with no modification?

@zih-syuan

Copy link
Copy Markdown
Author

Yes, this still uses the unmodified Verovio build from RISM Digital. This PR only changes how Neon configures the toolkit: for Hufnagel, it loads the custom font through Verovio's existing fontAddCustom option.

For @con, Neon currently uses a temporary workaround: it maps Hufnagel connections to @ligated before sending the MEI to Verovio, then maps them back to @con before saving/exporting. The saved MEI should still be correct, but the Verovio-facing path is still a shortcut, and I am going to replace it with proper @con handling.

@zih-syuan
zih-syuan requested a review from yinanazhou July 15, 2026 14:19
Comment thread src/Dashboard/Storage.ts Outdated
Comment on lines +29 to +32
// Sets <staffDef notationtype="neume.square"/"neume.hufnagel"> on the given
// MEI text. This is the only place a freshly-uploaded/created MEI file gets
// its notation type recorded - the Edit page's notation type dropdown does
// not lock or otherwise depend on this; it can still be changed afterward.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you not set staffDef@notationtype here?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. staffDef@notationtype is now set in ConvertMei.ts.

Comment thread src/utils/ConvertMei.ts Outdated
Comment on lines +66 to +72
function stripStaffDefNotationTypeSuffix(mei: Element): void {
const staffDef = mei.querySelector('staffDef');
const value = staffDef?.getAttribute('notationtype');
if (value === HUFNAGEL_NOTATIONTYPE || value === SQUARE_NOTATIONTYPE) {
staffDef.setAttribute('notationtype', 'neume');
}
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is only called once. I don't see why make this a function.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. I removed the function and inlined the logic in stripHufnagelForVerovio().

Comment thread src/utils/ConvertMei.ts Outdated
Comment on lines +74 to +87
// Restores a bare staffDef notationtype back to "neume.hufnagel"/"neume.square"
// given the notation type Neon already knows it's displaying.
function restoreStaffDefNotationTypeSuffix(
mei: Element,
notationType?: string,
): void {
if (notationType !== 'hufnagel' && notationType !== 'square') return;
mei
.querySelector('staffDef')
?.setAttribute(
'notationtype',
notationType === 'hufnagel' ? HUFNAGEL_NOTATIONTYPE : SQUARE_NOTATIONTYPE,
);
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see why making this a function as well. you can just append 'neume.' when setting it.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed the helper and now set notationtype directly as neume.${notationType}.

Comment thread src/DisplayPanel/DisplayControls.ts Outdated
* Set click listeners for Notation Type dropdown (Square / Hufnagel).
* Currently only updates the label and logs to console; engine wiring is next.
*/
export function setNotationTypeControls(neonView: NeonView): void {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You do not need NeonView here. You can dispatch the notationtypechange event (already happening below) and have DivaView/SingleView listen for it and call setNotationType themselves, since they already hold a neonView reference. That avoids threading the param through initDisplayControls, setDisplayListeners, DivaView.didLoad, and SingleView.onSVGLoad.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. DisplayControls now only dispatches notationtypechange. DivaView and SingleView listen for the event and call setNotationType() through their own neonView references, so the neonView parameter is no longer threaded through the display controls

Comment thread .gitignore Outdated

.idea

scripts/gen-hufnagel-grouping/

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this for?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed. It was only for a local glyph-generation scratch directory and is not needed by Neon.

Comment thread package.json Outdated
"diva.js": "github:DDMAL/diva.js#5f530483f653db68af1cbb92bc8cc967b51763c3",
"http-server": "^14.1.1",
"jsonschema": "^1.2.4",
"jszip": "^3.10.1",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be dropped. See comments above.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dropped. jszip and @types/jszip have been removed as direct dependencies.

Comment thread package.json
"dependencies": {
"d3": "^5.11.0",
"diva.js": "github:DDMAL/diva.js#master",
"diva.js": "github:DDMAL/diva.js#5f530483f653db68af1cbb92bc8cc967b51763c3",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why this specific version?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

package.json referenced #master, but that branch no longer exists, so installs that resolve the dependency afresh fail. The existing yarn.lock pointed to commit 5f530483, which was already known to work with Neon, so I pinned package.json to that commit as the temporary workaround described in issue #1365 .

Comment thread src/utils/GlyphIcons.ts Outdated

const cache = new Map<string, Promise<FontData>>();

async function loadFontData(zipUrl: string): Promise<FontData> {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You do not need jszip here. You can ship Hufnagel.woff as a flat asset instead of a zip and fetch + base64-encode it directly, since this only ever pulls out the font bytes. That drops the JSZip dependency and the unzip step from the worker, and since the font is static, you could even precompute the base64 at build time instead of re-fetching/encoding it every session. Also duplicated verbatim in VerovioWorker-dev.js, worth sharing one helper.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated. GlyphIcons.ts now loads flat .woff2 and bounding-box XML assets, so it no longer uses JSZip. I kept Hufnagel.zip in the Verovio workers because fontAddCustom expects the complete custom-font ZIP rather than only the font bytes. The production and dev workers now share a single base64-loading helper.

@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 46733732-a877-4f2e-8ebb-ea3df151fcf3

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch zih-syuan/hufnagel-ui

Comment @coderabbitai help to get the list of available commands.

@zih-syuan
zih-syuan requested a review from yinanazhou July 21, 2026 18:05
Comment thread src/Dashboard/Storage.ts
title: string,
mei: File,
bg: File,
notationType: string,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you still need notationType here? I don't think you need to touch this file.

);
const notationType = selectedNotationType?.value ?? 'square';

handleUploadAllDocuments(currentFolder, notationType)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you need notation type during upload?

);
return (
createManifest(id, newName, mei, image)
createManifest(id, newName, mei, image, notationType)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above. Why do you need notation type in manifest?

}
});

['square', 'hufnagel'].forEach((type) => {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks weird. I recommend you make notation type a type.

* HTML for each insert tab (neume, grouping, clef, system, and division).
*/
export const insertTabHtml: Record<string, string> = {
primitiveTab: `<p class="insert-element-container">

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this still used? Is it better for buildPrimitiveTabHtml to handle this as well?

}
}

function activateCurrentOrFirst(insertHandler: InsertHandler): void {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this a better naming for this function? It's not clear from the naming what it does. Also add a docstring maybe?

Comment thread src/NeonCore.ts
* only entries marked as dirty will be updated.
*/
async updateDatabase(): Promise<void> {
async updateDatabase(notationType?: string): Promise<void> {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is necessary. You can store the converted MEI in the database. You only need to restore it when exporting MEI.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Update INSERT panel icons for Hufnagel notation

4 participants