Skip to content

Numbered (Jianpu) notation: rendering aborts with TypeError on bars containing whole or longer rests #2803

Description

@SheperdHannigan

Summary

When a score is rendered with numbered (Jianpu) notation (staff.showNumbered = true), any bar whose only content is a whole, double-whole or quadruple-whole rest aborts rendering and the page stays blank:

TypeError: Cannot read properties of null (reading 'beat')
    at get beatOfHighestNote  (BeamingHelper)
    at LineBarRenderer.calculateBeamingOverflows
    at NumberedBarRenderer.calculateOverflows

The same score renders fine in standard notation. Since full-bar rests occur in essentially every multi-part arrangement, numbered notation is unusable for most ensemble scores.

Version

Reproduced on 1.8.4 (current stable). The relevant code paths are unchanged on main at the time of writing — beatOfLowestNote / beatOfHighestNote still use non-null assertions there.

Root cause

BeamingHelper.beatOfLowestNote / beatOfHighestNote dereference nullable fields through a non-null assertion:

public get beatOfLowestNote(): Beat {
    return this.lowestNoteInHelper!.beat;
}
public get beatOfHighestNote(): Beat {
    return this.highestNoteInHelper!.beat;
}

lowestNoteInHelper / highestNoteInHelper are only ever assigned by _checkNote(), and checkBeat() only calls it for non-rest beats. For a rest-only helper both remain null.

Normally those getters are unreachable for rest-only helpers, because LineBarRenderer.calculateBeamingOverflows only enters the branch using them when shouldPaintBeamingHelper(h) is true, and the base implementation excludes exactly that case:

protected shouldPaintBeamingHelper(h: BeamingHelper): boolean {
    return !h.isRestBeamHelper;
}

NumberedBarRenderer overrides it unconditionally:

protected override shouldPaintBeamingHelper(_h: BeamingHelper): boolean {
    return true;
}

so rest-only helpers now reach the branch chain in calculateBeamingOverflows:

} else if (h.beats.length === 1 && h.beats[0].duration >= Duration.Half) {
    // flag-based overflow: only touches h.beats[0] — safe
} else {
    // uses h.beatOfLowestNote / h.beatOfHighestNote — null deref for rest-only helpers
}

The duration >= Duration.Half test does shield short single-rest helpers, but Duration is numbered by denominator, so the long durations are negative or below Half:

QuadrupleWhole = -4
DoubleWhole    = -2
Whole          =  1
Half           =  2
Quarter        =  4

Whole, DoubleWhole and QuadrupleWhole therefore all fail >= Duration.Half and fall through into the unsafe else.

Reproduction

Render any score containing a bar whose only content is a whole rest with staff.showNumbered = true — for example a four-part brass arrangement in which individual parts rest for full bars.

Possible fixes

Whichever direction you prefer:

  1. Make the getters null-safe. A rest-only helper always has exactly one beat, so it can serve as the fallback:
    public get beatOfLowestNote(): Beat {
        return this.lowestNoteInHelper?.beat ?? this.beats[0];
    }
    VoiceContainerGlyph.getLowestNoteY / getHighestNoteY already return 0 when no container is found (i.e. contribute no overflow), and for a rest beat they yield the rest's own extent. Both outcomes seem reasonable, and no "helper has notes" case changes behaviour.
  2. Keep the base guard for rest-only helpers in NumberedBarRenderer.shouldPaintBeamingHelper.
  3. Widen the guard in calculateBeamingOverflows so it tests isRestBeamHelper (or ModelUtils.getIndex(duration)) instead of comparing the raw enum value.

I am currently working around this downstream with a build-time patch equivalent to option 1; happy to provide more detail if useful.

Activity

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

Metadata

Metadata

Assignees

Labels

state-template-not-usedIssue Template not used as required or wrong template used

Type

Fields

Priority

Low

Effort

Low

Area

Rendering

Platform

All / Multiple

Work State

Needs More Info

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions