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:
- 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.
- Keep the base guard for rest-only helpers in
NumberedBarRenderer.shouldPaintBeamingHelper.
- 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.
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: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
mainat the time of writing —beatOfLowestNote/beatOfHighestNotestill use non-null assertions there.Root cause
BeamingHelper.beatOfLowestNote/beatOfHighestNotedereference nullable fields through a non-null assertion:lowestNoteInHelper/highestNoteInHelperare only ever assigned by_checkNote(), andcheckBeat()only calls it for non-rest beats. For a rest-only helper both remainnull.Normally those getters are unreachable for rest-only helpers, because
LineBarRenderer.calculateBeamingOverflowsonly enters the branch using them whenshouldPaintBeamingHelper(h)is true, and the base implementation excludes exactly that case:NumberedBarRendereroverrides it unconditionally:so rest-only helpers now reach the branch chain in
calculateBeamingOverflows:The
duration >= Duration.Halftest does shield short single-rest helpers, butDurationis numbered by denominator, so the long durations are negative or belowHalf:Whole,DoubleWholeandQuadrupleWholetherefore all fail>= Duration.Halfand fall through into the unsafeelse.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:
VoiceContainerGlyph.getLowestNoteY/getHighestNoteYalready 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.NumberedBarRenderer.shouldPaintBeamingHelper.calculateBeamingOverflowsso it testsisRestBeamHelper(orModelUtils.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.