From f085cf89c8c9df4e2ca1db76982a75d17e576e38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?La=C3=A9rcio=20de=20Sousa?= Date: Mon, 2 Mar 2026 11:11:16 -0300 Subject: [PATCH 1/7] Add configurable NABC horizontal alignment (left/center) --- CHANGELOG.md | 1 + doc/Command_Index_User.tex | 12 +++++-- doc/Command_Index_internal.tex | 53 ++++++++++++++++++++++++++--- tex/gregoriotex-main.tex | 27 +++++++++++++-- tex/gregoriotex-nabc.lua | 15 ++++++++- tex/gregoriotex-nabc.tex | 61 +++++++++++++++++++++++++++++++++- tex/gregoriotex-syllable.tex | 5 +-- 7 files changed, 162 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 363a8715..a7791827 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ As of v3.0.0 this project adheres to [Semantic Versioning](http://semver.org/). - Added horizontal spacing preservation for NABC neumes, preventing overlap. Solves [#1699](https://github.com/gregorio-project/gregorio/issues/1699). - Added overtie/undertie special symbols (`ut` for `\greundertie`, `ot` for `\greovertie`, and `dt` for `\gredoubletie`), and a configurable lyric tying shorthand (`~` for `\GreLyricTie`). - Added support for the C23 standard (the default in GCC 15). The included build scripts continue to default to GNU89 C. +- Added optional second argument to `\gresetnabcalignment` for horizontal alignment (`left` or `center`). In `center` mode the NABC neume is centered on the GABC note group; in `neume`+`center` mode both left-side and right-side significative letters are excluded from the centering computation. See [#1665](https://github.com/gregorio-project/gregorio/issues/1665). ### Fixed - Fixed a bug that could cause a punctum mora that is supposed to be below the line (`.0`) to appear above the line. This bug was platform-dependent and was observed on a Windows system. See [#1642](https://github.com/gregorio-project/gregorio/issues/1642). diff --git a/doc/Command_Index_User.tex b/doc/Command_Index_User.tex index 259e226e..7f87b65f 100644 --- a/doc/Command_Index_User.tex +++ b/doc/Command_Index_User.tex @@ -1294,13 +1294,21 @@ \subsubsection{Sign printing} \textbf{Important:} NABC glyphs visibility is controlled independently from notes visibility (\verb=\gresetnotes=). This allows you to hide the main notes (i.e. using \verb=\gresetnotes{invisible}= or some \verb=\gresetnotes{?phantom}= variant) while keeping NABC glyphs visible, which is useful for producing NABC-only output with lyrics. -\macroname{\textbackslash gresetnabcalignment}{[\#1]\{\#2\}}{gregoriotex-nabc.tex} -Sets the horizontal alignment reference for nabc neumes relative to the gabc notes below. By default, nabc neumes are aligned to the left edge of the entire complex glyph descriptor (including significative letters), which can produce aesthetically suboptimal results when there are significative letters to the left of the neume. The optional voice parameter allows setting the alignment independently for each nabc voice. +\macroname{\textbackslash gresetnabcalignment}{[\#1]\{\#2\}\{\#3\}}{gregoriotex-nabc.tex} +Sets the alignment reference and horizontal alignment of nabc neumes relative to the gabc notes below. + +The first mandatory argument (\#2) controls the \emph{alignment reference}: which part of the nabc complex glyph descriptor is used as anchor. By default, the entire complex glyph descriptor (including significative letters) is used; with \texttt{neume}, only the neume body (basic glyph descriptor + prepunctis) is considered, which avoids visual misalignment caused by significative letters. + +The second argument (\#3) is \emph{optional}: if omitted, the current horizontal alignment is not changed. This allows calling \verb=\gresetnabcalignment{full}= to change only the alignment reference while preserving the horizontal setting. When provided, it controls the \emph{horizontal alignment} of the nabc neume relative to the gabc note group. + +When \texttt{center} is combined with \texttt{neume} mode, both left-side and right-side significative letters are excluded from the centering calculation, so that the neume body is centered on the gabc glyph. \begin{argtable} \#1 & integer & (Optional) The nabc voice number: 1 = above staff, 2 = below staff. If omitted, sets the default for all voices and clears any per-voice overrides.\\ \#2 & \texttt{neume} & Align to the neume (basic glyph descriptor + prepunctis), ignoring significative letters.\\ & \texttt{full} & Align to the entire complex glyph descriptor (default).\\ + \#3 & \texttt{left} & (Optional) Left-align the nabc neume with the gabc glyph (default).\\ + & \texttt{center} & Center the nabc neume on the gabc glyph.\\ \end{argtable} \macroname{\textbackslash gresetnabcskipalterations}{[\#1]\{\#2\}}{gregoriotex-nabc.tex} diff --git a/doc/Command_Index_internal.tex b/doc/Command_Index_internal.tex index e92569b0..f8e85006 100644 --- a/doc/Command_Index_internal.tex +++ b/doc/Command_Index_internal.tex @@ -1479,11 +1479,23 @@ \section{Gregorio\TeX{} Controls} \#2 & box register & Box register to save the rendered content to\\ \end{argtable} +\macroname{\textbackslash gre@nabc@halign@content}{\#1\#2\#3}{gregoriotex-main.tex} +Helper that applies horizontal alignment inside a zero-width NABC hbox. Used by \verb=\gre@nabc@place@voice@i= and \verb=\gre@nabc@place@voice@ii= to shift the NABC content according to the chosen horizontal alignment mode. + +\begin{argtable} + \#1 & count register & The horizontal alignment register: 0 = left, 1 = center\\ + \#2 & box register & The NABC box (must still contain its content)\\ + \#3 & dimen & Per-voice right-side significative letter overflow (\verb=\gre@dimen@nabcrightoverflow@i= or \verb=\gre@dimen@nabcrightoverflow@ii=). In \texttt{neume} mode this width is added back to the centering formula so that right-side LS are excluded from centering, mirroring the left-side exclusion\\ +\end{argtable} + +The centering formula is: +\verb=\kern\dimexpr(\gre@dimen@lastglyphwidth - \wd#2 + #3) / 2\relax= + \macroname{\textbackslash gre@nabc@place@voice@i}{}{gregoriotex-main.tex} -Phase 2 of NABC two-phase processing for voice 1 (above staff): places the pre-rendered NABC voice 1 content from \textbackslash gre@box@nabc@voice@i at the correct vertical position above the staff. +Phase 2 of NABC two-phase processing for voice 1 (above staff): places the pre-rendered NABC voice 1 content from \verb=\gre@box@nabc@voice@i= at the correct vertical position above the staff. Passes \verb=\gre@count@nabc@halign@i= and \verb=\gre@dimen@nabcrightoverflow@i= to \verb=\gre@nabc@halign@content= for horizontal alignment. \macroname{\textbackslash gre@nabc@place@voice@ii}{}{gregoriotex-main.tex} -Phase 2 of NABC two-phase processing for voice 2 (below staff): places the pre-rendered NABC voice 2 content from \textbackslash gre@box@nabc@voice@ii at the correct vertical position below the staff. +Phase 2 of NABC two-phase processing for voice 2 (below staff): places the pre-rendered NABC voice 2 content from \verb=\gre@box@nabc@voice@ii= at the correct vertical position below the staff. Passes \verb=\gre@count@nabc@halign@ii= and \verb=\gre@dimen@nabcrightoverflow@ii= to \verb=\gre@nabc@halign@content= for horizontal alignment. \macroname{\textbackslash gre@nabc@pre@voice@i}{}{gregoriotex-main.tex} Deferred macro for phase~1 processing of NABC voice~1 (above staff). Set by @@ -1511,7 +1523,7 @@ \section{Gregorio\TeX{} Controls} \verb=\gre@nabc@emit@voice@i= for voice~2, guarded by \verb=\ifgre@nabc@voice@ii@ready=. \macroname{\textbackslash gre@setnabcalignment@global}{\#1}{gregoriotex-nabc.tex} -Sets the default NABC horizontal alignment mode for all voices and clears any per-voice overrides. +Sets the default NABC alignment reference mode for all voices and clears any per-voice overrides. \begin{argtable} \#1 & \texttt{neume} & Align to the neume (basic glyph + prepunctis), ignoring significative letters\\ @@ -1519,7 +1531,7 @@ \section{Gregorio\TeX{} Controls} \end{argtable} \macroname{\textbackslash gre@setnabcalignment@voice}{[\#1]\{\#2\}}{gregoriotex-nabc.tex} -Sets the NABC horizontal alignment mode for a specific voice. +Sets the NABC alignment reference mode for a specific voice. \begin{argtable} \#1 & integer & The nabc voice number: 1 = above staff, 2 = below staff\\ @@ -1527,6 +1539,27 @@ \section{Gregorio\TeX{} Controls} & \texttt{full} & Align to the entire complex glyph descriptor (default)\\ \end{argtable} +\macroname{\textbackslash gre@setnabchalign@global}{\#1}{gregoriotex-nabc.tex} +Sets the default NABC horizontal alignment for all voices. Called by +\verb=\gresetnabcalignment= when an optional second argument is provided +without a voice specifier. + +\begin{argtable} + \#1 & \texttt{left} & Left-align the NABC neume with the GABC glyph (default)\\ + & \texttt{center} & Center the NABC neume on the GABC glyph\\ +\end{argtable} + +\macroname{\textbackslash gre@setnabchalign@voice}{[\#1]\{\#2\}}{gregoriotex-nabc.tex} +Sets the NABC horizontal alignment for a specific voice. Called by +\verb=\gresetnabcalignment= when both a voice specifier and a second argument +are provided. + +\begin{argtable} + \#1 & integer & The nabc voice number: 1 = above staff, 2 = below staff\\ + \#2 & \texttt{left} & Left-align the NABC neume with the GABC glyph (default)\\ + & \texttt{center} & Center the NABC neume on the GABC glyph\\ +\end{argtable} + \macroname{\textbackslash gre@setnabcskipalterations@global}{\#1}{gregoriotex-nabc.tex} Sets the skip-alterations flag for all NABC voices simultaneously. Internal implementation of \verb=\gresetnabcskipalterations= when called without the @@ -2441,6 +2474,18 @@ \subsection{Distances} \macroname{\textbackslash gre@dimen@nabcleftoverflow}{}{gregoriotex-nabc.tex} Dimension for communicating NABC left overflow from Lua to \TeX. When NABC alignment mode is \texttt{neume}, significative letters on the left side of a neume extend to the left of the neume body. This dimension holds the overflow width (set by Lua code during NABC rendering) so that the \TeX\ layer can reserve space at the note level to prevent overlap with previous elements. Reset to 0pt after each syllable. +\macroname{\textbackslash gre@dimen@nabcrightoverflow@i}{}{gregoriotex-nabc.tex} +Per-voice dimension for communicating the width of right-side significative letters (positions 3, 6, 9) from Lua to \TeX\ for voice~1. In \texttt{neume} alignment mode, the Lua code sets this to the maximum right-side LS width scaled by the NABC font scale. In \texttt{full} mode (or when there are no right-side LS), it is set to 0pt. Used by \verb=\gre@nabc@halign@content= to exclude right-side LS from the centering computation, mirroring the left-side exclusion. + +\macroname{\textbackslash gre@dimen@nabcrightoverflow@ii}{}{gregoriotex-nabc.tex} +Same as \verb=\gre@dimen@nabcrightoverflow@i= but for voice~2 (below staff). + +\macroname{\textbackslash gre@count@nabc@halign@i}{}{gregoriotex-nabc.tex} +Count register holding the horizontal alignment mode for voice~1. Values: 0 = left (default), 1 = center. Set by \verb=\gre@setnabchalign@global= or \verb=\gre@setnabchalign@voice=. + +\macroname{\textbackslash gre@count@nabc@halign@ii}{}{gregoriotex-nabc.tex} +Count register holding the horizontal alignment mode for voice~2. Values: 0 = left (default), 1 = center. Set by \verb=\gre@setnabchalign@global= or \verb=\gre@setnabchalign@voice=. + \macroname{\textbackslash gre@dimen@nabckernemitted}{}{gregoriotex-nabc.tex} Tracks the total kern emitted at the note level across all NABC voices for the current syllable. Used to coordinate left overflow handling between voice 1 (above staff) and voice 2 (below staff), ensuring that only the maximum overflow is applied. Reset to 0pt after each syllable. diff --git a/tex/gregoriotex-main.tex b/tex/gregoriotex-main.tex index fc812825..15366d9c 100644 --- a/tex/gregoriotex-main.tex +++ b/tex/gregoriotex-main.tex @@ -718,13 +718,34 @@ \global\setbox#2=\hbox{\unhbox\gre@box@nabctemp}% }% +% Helper: apply horizontal alignment kern inside a zero-width NABC hbox. +% #1: halign count register (0=left, 1=center) +% #2: nabc box register (must still contain the box) +% #3: per-voice right-side LS overflow dimension (\gre@dimen@nabcrightoverflow@i/ii) +% The kern shifts the NABC content to the right inside the zero-width hbox +% so that the neume aligns with the GABC glyph according to the chosen mode. +% \gre@dimen@lastglyphwidth must be set to the current GABC glyph width. +\def\gre@nabc@halign@content#1#2#3{% + \ifcase#1\relax + % 0 = left: no offset (current default behavior) + \unhbox#2% + \or + % 1 = center: kern = (glyphwidth - (nabcwidth - rightoverflow)) / 2 + % In neume mode, rightoverflow excludes right-side LS from centering. + \kern\dimexpr(\gre@dimen@lastglyphwidth - \wd#2 + #3) / 2\relax + \unhbox#2% + \fi +}% + % Phase 2: Place voice 1 (above staff) \def\gre@nabc@place@voice@i{% \gre@attr@part=7\relax% \gre@dimen@temp@five=\dimexpr(\gre@dimen@staffheight % + \gre@space@dimen@spacebeneathtext % + \gre@space@dimen@spacelinestext)\relax% - \leavevmode\raise\gre@dimen@temp@five\hbox to 0pt{\unhbox\gre@box@nabc@voice@i\hss}% + \leavevmode\raise\gre@dimen@temp@five\hbox to 0pt{% + \gre@nabc@halign@content{\gre@count@nabc@halign@i}{\gre@box@nabc@voice@i}{\gre@dimen@nabcrightoverflow@i}% + \hss}% \unsetattribute{\gre@attr@part}% }% @@ -732,7 +753,9 @@ \def\gre@nabc@place@voice@ii{% \gre@dimen@temp@five=\dimexpr(\gre@space@dimen@spacebeneathtext + \gre@space@dimen@spacelinestext)\relax - \leavevmode\raise\gre@dimen@temp@five\hbox attr \gre@attrid@part=8 to 0pt{\unhbox\gre@box@nabc@voice@ii\hss}% + \leavevmode\raise\gre@dimen@temp@five\hbox attr \gre@attrid@part=8 to 0pt{% + \gre@nabc@halign@content{\gre@count@nabc@halign@ii}{\gre@box@nabc@voice@ii}{\gre@dimen@nabcrightoverflow@ii}% + \hss}% }% % Deferred macros for two-phase processing. diff --git a/tex/gregoriotex-nabc.lua b/tex/gregoriotex-nabc.lua index b95f900f..bf042226 100644 --- a/tex/gregoriotex-nabc.lua +++ b/tex/gregoriotex-nabc.lua @@ -462,11 +462,24 @@ local gregallparse_neumes = function(str, kind, scale, voice) -- Also set \gre@dimen@nabcleftoverflow so the TeX layer can -- reserve space at the note level and prevent overlap with the -- previous element. - if get_nabc_alignment(voice) == 'neume' and lwidths[10] > 0 then + local is_neume_mode = (get_nabc_alignment(voice) == 'neume') + if is_neume_mode and lwidths[10] > 0 then local overflow_sp = string.format("%.3f", lwidths[10] * scale) base = '\\global\\gre@dimen@nabcleftoverflow=' .. overflow_sp .. 'sp' .. '\\kern -' .. overflow_sp .. 'sp' .. base end + -- Communicate right-side significative letter width to TeX. + -- In 'neume' + 'center' mode, the centering computation ignores + -- this width so the neume glyph is centered on the GABC note + -- group, mirroring the left-side exclusion. + local rdim = '\\gre@dimen@nabcrightoverflow@' + .. (voice == 1 and 'i' or 'ii') + if is_neume_mode and lwidths[12] > 0 then + base = base .. '\\global' .. rdim .. '=' + .. string.format("%.3f", lwidths[12] * scale) .. 'sp' + else + base = base .. '\\global' .. rdim .. '=0sp' + end end end ret = ret .. base diff --git a/tex/gregoriotex-nabc.tex b/tex/gregoriotex-nabc.tex index 63d49099..2ea8ddf6 100644 --- a/tex/gregoriotex-nabc.tex +++ b/tex/gregoriotex-nabc.tex @@ -121,6 +121,25 @@ \newif\ifgre@nabc@suppress@leftoverflow \gre@nabc@suppress@leftoverflowfalse +% Horizontal alignment of NABC neumes relative to the GABC note group. +% Values: 0 = left (default), 1 = center. +% Per-voice registers; set via the optional second argument of +% \gresetnabcalignment. +\newcount\gre@count@nabc@halign@i +\gre@count@nabc@halign@i=0\relax +\newcount\gre@count@nabc@halign@ii +\gre@count@nabc@halign@ii=0\relax + +% Per-voice dimensions for right-side significative letter overflow. +% In 'neume' mode, the Lua code sets these to the width of right-side LS +% (positions 3, 6, 9). The center alignment computation subtracts this +% width so that centering is based on the neume glyph only, ignoring +% right-side significative letters (mirroring the left-side exclusion). +\newdimen\gre@dimen@nabcrightoverflow@i +\gre@dimen@nabcrightoverflow@i=0pt\relax +\newdimen\gre@dimen@nabcrightoverflow@ii +\gre@dimen@nabcrightoverflow@ii=0pt\relax + \def\gresetnabc#1#2{% \gre@processvisibilitywithphantom{#2}% {\csname gre@nabcvoice@\romannumeral#1@visibletrue\endcsname}% @@ -130,6 +149,16 @@ } % See Command_Index_User.tex for documentation. +% \gresetnabcalignment{neume|full} +% \gresetnabcalignment{neume|full}{left|center} +% \gresetnabcalignment[voice]{neume|full} +% \gresetnabcalignment[voice]{neume|full}{left|center} +% +% The first argument selects the vertical alignment mode (significative-letter +% handling). The optional second argument selects the horizontal alignment +% of the NABC neume relative to the GABC note group: 'left' (default) or +% 'center'. When the second argument is omitted the horizontal alignment +% is left unchanged. \def\gresetnabcalignment{\@ifnextchar[{\gre@setnabcalignment@voice}{\gre@setnabcalignment@global}}% \def\gre@setnabcalignment@global#1{% \IfStrEqCase{#1}{% @@ -140,6 +169,8 @@ }[% all other cases \gre@error{Unrecognized option "#1" for \protect\gresetnabcalignment\MessageBreak Possible options are: 'neume' and 'full'}% ]% + % Check for optional second argument (horizontal alignment) + \@ifnextchar\bgroup{\gre@setnabchalign@global}{}% }% \def\gre@setnabcalignment@voice[#1]#2{% \IfStrEqCase{#2}{% @@ -150,7 +181,35 @@ }[% all other cases \gre@error{Unrecognized option "#2" for \protect\gresetnabcalignment\MessageBreak Possible options are: 'neume' and 'full'}% ]% -} + % Check for optional second argument (horizontal alignment) + \@ifnextchar\bgroup{\gre@setnabchalign@voice[#1]}{}% +}% + +% Internal helpers for horizontal alignment (called from \gresetnabcalignment) +\def\gre@setnabchalign@global#1{% + \IfStrEqCase{#1}{% + {left}{% + \global\gre@count@nabc@halign@i=0\relax + \global\gre@count@nabc@halign@ii=0\relax}% + {center}{% + \global\gre@count@nabc@halign@i=1\relax + \global\gre@count@nabc@halign@ii=1\relax}% + }[% + \gre@error{Unrecognized horizontal alignment "#1" for \protect\gresetnabcalignment\MessageBreak Possible options are: 'left' and 'center'}% + ]% +}% +\def\gre@setnabchalign@voice[#1]#2{% + \IfStrEqCase{#2}{% + {left}{% + \ifnum#1=1\relax\global\gre@count@nabc@halign@i=0\relax\fi + \ifnum#1=2\relax\global\gre@count@nabc@halign@ii=0\relax\fi}% + {center}{% + \ifnum#1=1\relax\global\gre@count@nabc@halign@i=1\relax\fi + \ifnum#1=2\relax\global\gre@count@nabc@halign@ii=1\relax\fi}% + }[% + \gre@error{Unrecognized horizontal alignment "#2" for \protect\gresetnabcalignment\MessageBreak Possible options are: 'left' and 'center'}% + ]% +}% % See Command_Index_User.tex for documentation. \def\gresetnabcskipalterations{% diff --git a/tex/gregoriotex-syllable.tex b/tex/gregoriotex-syllable.tex index d1b67584..959498f4 100644 --- a/tex/gregoriotex-syllable.tex +++ b/tex/gregoriotex-syllable.tex @@ -171,10 +171,11 @@ % #6 are the signs to typeset after the glyph (almost all signs) % #7 is the line:char:column for a textedit link \def\GreGlyph#1#2#3#4#5#6#7{% - \gre@newglyphcommon % - % Create box containing the new glyph. + % Pre-measure the glyph so that \gre@dimen@lastglyphwidth is available + % during NABC placement (needed for center/right horizontal alignment). \setbox\gre@box@temp@width=\hbox{\gre@pointandclick{\gre@font@music #1}{#7}}% \global\gre@dimen@lastglyphwidth=\wd\gre@box@temp@width % + \gre@newglyphcommon % % the three next lines are a trick to get the additional lines below the glyphs \gre@skip@temp@one = \gre@dimen@lastglyphwidth\relax% \kern\gre@skip@temp@one % From 2ebd7e9ba778ccda1c2c58137433747d89729c08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?La=C3=A9rcio=20de=20Sousa?= Date: Thu, 5 Mar 2026 15:51:06 -0300 Subject: [PATCH 2/7] Center-align NABC over full element advance, not first glyph only When \gresetnabcalignment{neume}{center} is active, NABC neumes were centered over the width of the first glyph only. For multi-glyph elements (e.g. tristropha jjj) and multi-element neumes (e.g. scandicus flexus fh/ji), this produced visually wrong centering. The fix defers NABC placement when center alignment is active. The element advance (all glyph widths + inter-glyph/inter-element spacing) is accumulated through GreEndOfGlyph and GreEndOfElement. At end of syllable (or next GreNABCNeumes), the deferred NABC is placed centered over the full accumulated advance. During the boxing pass and tight rendering, immediate placement is used (no deferral) since the element advance is not meaningful there. --- tex/gregoriotex-main.tex | 61 +++++++++++++++++++++++++++++++++--- tex/gregoriotex-nabc.tex | 15 +++++++++ tex/gregoriotex-syllable.tex | 9 ++++++ 3 files changed, 81 insertions(+), 4 deletions(-) diff --git a/tex/gregoriotex-main.tex b/tex/gregoriotex-main.tex index 15366d9c..da0a3a36 100644 --- a/tex/gregoriotex-main.tex +++ b/tex/gregoriotex-main.tex @@ -737,8 +737,8 @@ \fi }% -% Phase 2: Place voice 1 (above staff) -\def\gre@nabc@place@voice@i{% +% Immediate placement helper for voice 1 (always places, no deferral check). +\def\gre@nabc@place@voice@i@now{% \gre@attr@part=7\relax% \gre@dimen@temp@five=\dimexpr(\gre@dimen@staffheight % + \gre@space@dimen@spacebeneathtext % @@ -748,15 +748,65 @@ \hss}% \unsetattribute{\gre@attr@part}% }% +% Phase 2: Place voice 1 (above staff) — with deferral for center alignment. +\def\gre@nabc@place@voice@i{% + \ifgre@boxing + \gre@nabc@place@voice@i@now + \else\ifnum\gre@count@nabc@halign@i=1\relax + \global\gre@nabc@halign@pending@itrue + \ifgre@nabc@halign@pending\else + \global\gre@nabc@halign@pendingtrue + \global\gre@dimen@nabc@elementadvance=0pt\relax + \fi + \else + \gre@nabc@place@voice@i@now + \fi\fi +}% -% Phase 2: Place voice 2 (below staff) -\def\gre@nabc@place@voice@ii{% +% Immediate placement helper for voice 2 (always places, no deferral check). +\def\gre@nabc@place@voice@ii@now{% \gre@dimen@temp@five=\dimexpr(\gre@space@dimen@spacebeneathtext + \gre@space@dimen@spacelinestext)\relax \leavevmode\raise\gre@dimen@temp@five\hbox attr \gre@attrid@part=8 to 0pt{% \gre@nabc@halign@content{\gre@count@nabc@halign@ii}{\gre@box@nabc@voice@ii}{\gre@dimen@nabcrightoverflow@ii}% \hss}% }% +% Phase 2: Place voice 2 (below staff) — with deferral for center alignment. +\def\gre@nabc@place@voice@ii{% + \ifgre@boxing + \gre@nabc@place@voice@ii@now + \else\ifnum\gre@count@nabc@halign@ii=1\relax + \global\gre@nabc@halign@pending@iitrue + \ifgre@nabc@halign@pending\else + \global\gre@nabc@halign@pendingtrue + \global\gre@dimen@nabc@elementadvance=0pt\relax + \fi + \else + \gre@nabc@place@voice@ii@now + \fi\fi +}% + +% Flush deferred center-aligned NABC placement. +% Kerns backwards by the accumulated element advance so that the NABC is +% placed at the position of the first glyph, centered over the full advance. +\def\gre@nabc@flush@deferred@halign{% + \ifgre@nabc@halign@pending + \kern-\gre@dimen@nabc@elementadvance\relax + \begingroup + \gre@dimen@lastglyphwidth=\gre@dimen@nabc@elementadvance\relax + \ifgre@nabc@halign@pending@i + \gre@nabc@place@voice@i@now + \fi + \ifgre@nabc@halign@pending@ii + \gre@nabc@place@voice@ii@now + \fi + \endgroup + \kern\gre@dimen@nabc@elementadvance\relax + \global\gre@nabc@halign@pendingfalse + \global\gre@nabc@halign@pending@ifalse + \global\gre@nabc@halign@pending@iifalse + \fi +}% % Deferred macros for two-phase processing. % pre = Phase 1, emit = Phase 2. Set by \GreSetNabcAboveLines / BelowLines, @@ -1639,6 +1689,9 @@ \GreNoBreak % \gre@get@spaceskip{#1}% \gre@hskip\gre@skip@temp@four % + \ifgre@nabc@halign@pending + \global\advance\gre@dimen@nabc@elementadvance by \gre@skip@temp@four\relax + \fi \GreNoBreak % \relax% }% diff --git a/tex/gregoriotex-nabc.tex b/tex/gregoriotex-nabc.tex index 2ea8ddf6..ae46cb4d 100644 --- a/tex/gregoriotex-nabc.tex +++ b/tex/gregoriotex-nabc.tex @@ -140,6 +140,19 @@ \newdimen\gre@dimen@nabcrightoverflow@ii \gre@dimen@nabcrightoverflow@ii=0pt\relax +% Element-wide horizontal advance tracking for center alignment of +% multi-glyph and multi-element neumes. When center alignment is active, +% NABC placement is deferred to the end of the NABC scope so that +% centering uses the full element advance, not just the first glyph width. +\newdimen\gre@dimen@nabc@elementadvance +\gre@dimen@nabc@elementadvance=0pt\relax +\newif\ifgre@nabc@halign@pending +\gre@nabc@halign@pendingfalse +\newif\ifgre@nabc@halign@pending@i +\gre@nabc@halign@pending@ifalse +\newif\ifgre@nabc@halign@pending@ii +\gre@nabc@halign@pending@iifalse + \def\gresetnabc#1#2{% \gre@processvisibilitywithphantom{#2}% {\csname gre@nabcvoice@\romannumeral#1@visibletrue\endcsname}% @@ -241,6 +254,7 @@ } \def\GreNABCNeumes#1#2#3#4{% + \gre@nabc@flush@deferred@halign \csname ifgre@nabcvoice@\romannumeral#1@visible\endcsname % \ifcase#1\relax \or % 1 @@ -261,6 +275,7 @@ \gre@nabc@pre@voice@ii \gre@nabc@emit@voice@i \gre@nabc@emit@voice@ii + \gre@nabc@flush@deferred@halign } \newif\ifgre@nabcfontloaded% diff --git a/tex/gregoriotex-syllable.tex b/tex/gregoriotex-syllable.tex index 959498f4..eb1b11a6 100644 --- a/tex/gregoriotex-syllable.tex +++ b/tex/gregoriotex-syllable.tex @@ -108,6 +108,10 @@ \fi% \gre@nabc@emit@voice@i \gre@nabc@emit@voice@ii + % Track element advance for deferred center alignment. + \ifgre@nabc@halign@pending + \global\advance\gre@dimen@nabc@elementadvance by \gre@dimen@lastglyphwidth\relax + \fi % Reset NABC overflow accumulators after all voices have been processed. % During boxing, keep values so the adjustment code can read them. \ifgre@boxing\else% @@ -497,6 +501,9 @@ \gre@suppress@bars \def\GreEndOfElement##1##2##3{}% \def\GreEndOfGlyph##1{}% + % Disable deferred centering: no glyph widths to track in tight mode. + \let\gre@nabc@place@voice@i\gre@nabc@place@voice@i@now + \let\gre@nabc@place@voice@ii\gre@nabc@place@voice@ii@now #1% \endgroup }% @@ -1266,6 +1273,7 @@ {\raise 12pt\hbox to 0pt{\rule{0.4pt}{12pt}\hss}}% {}% do nothing if not debugging #9% we do that instead of \unhbox\Syllablnotes, because it would not set the \localrightbox + \gre@nabc@flush@deferred@halign % Apply per-syllable NABC kern computed in \gre@syllablenotes. \ifdim\gre@dimen@nabc@extrakern>0pt\relax \kern\gre@dimen@nabc@extrakern @@ -1564,6 +1572,7 @@ {\raise 12pt\hbox to 0pt{\rule{0.4pt}{12pt}\hss}}% {}% do nothing if not debugging #9% + \gre@nabc@flush@deferred@halign \IfSubStr{\gre@debug}{,barspacing,}% % when debugging we add a zero-width line to mark the syllable bound {\raise 12pt\hbox to 0pt{\rule{0.4pt}{12pt}\hss}}% From 9d9e2387e822eacae9d2eaa8fdcc9c3a2e2f21d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?La=C3=A9rcio=20de=20Sousa?= Date: Thu, 5 Mar 2026 18:33:48 -0300 Subject: [PATCH 3/7] Per-voice deferred flush: extend centering across empty NABC elements When center alignment is active and a voice's NABC is empty on the next element, the centering now extends to span that element as well. Previously, \GreNABCNeumes flushed ALL pending voices at once; now it flushes only the voice being replaced, allowing other voices to keep accumulating advance. Changes: - Add per-voice advance start registers (\gre@dimen@nabc@advance@start@i/ii) that record the element advance at the moment each voice enters pending state - New \gre@nabc@flush@deferred@voice{} macro flushes a single voice: kerns back by (advance - start), centers, kerns forward - \gre@nabc@flush@deferred@halign now delegates to per-voice flush - \GreNABCNeumes flushes only the voice matching #1 instead of all - \gre@nabc@place@voice@i/ii record advance start when entering pending --- tex/gregoriotex-main.tex | 46 +++++++++++++++++++++++++--------------- tex/gregoriotex-nabc.tex | 15 ++++++++++++- 2 files changed, 43 insertions(+), 18 deletions(-) diff --git a/tex/gregoriotex-main.tex b/tex/gregoriotex-main.tex index da0a3a36..d68cc185 100644 --- a/tex/gregoriotex-main.tex +++ b/tex/gregoriotex-main.tex @@ -758,6 +758,7 @@ \global\gre@nabc@halign@pendingtrue \global\gre@dimen@nabc@elementadvance=0pt\relax \fi + \global\gre@dimen@nabc@advance@start@i=\gre@dimen@nabc@elementadvance\relax \else \gre@nabc@place@voice@i@now \fi\fi @@ -781,30 +782,41 @@ \global\gre@nabc@halign@pendingtrue \global\gre@dimen@nabc@elementadvance=0pt\relax \fi + \global\gre@dimen@nabc@advance@start@ii=\gre@dimen@nabc@elementadvance\relax \else \gre@nabc@place@voice@ii@now \fi\fi }% -% Flush deferred center-aligned NABC placement. -% Kerns backwards by the accumulated element advance so that the NABC is -% placed at the position of the first glyph, centered over the full advance. -\def\gre@nabc@flush@deferred@halign{% - \ifgre@nabc@halign@pending - \kern-\gre@dimen@nabc@elementadvance\relax +% Flush deferred center-aligned NABC placement for a single voice. +% #1 = voice suffix (i or ii). +% Kerns back from current position to the voice's start, centers over +% that voice's span, then kerns forward to the current position. +\def\gre@nabc@flush@deferred@voice#1{% + \csname ifgre@nabc@halign@pending@#1\endcsname \begingroup - \gre@dimen@lastglyphwidth=\gre@dimen@nabc@elementadvance\relax - \ifgre@nabc@halign@pending@i - \gre@nabc@place@voice@i@now - \fi - \ifgre@nabc@halign@pending@ii - \gre@nabc@place@voice@ii@now - \fi + \gre@dimen@temp@five=\dimexpr\gre@dimen@nabc@elementadvance + - \csname gre@dimen@nabc@advance@start@#1\endcsname\relax + \kern-\gre@dimen@temp@five\relax + \gre@dimen@lastglyphwidth=\gre@dimen@temp@five\relax + \csname gre@nabc@place@voice@#1@now\endcsname + \kern\gre@dimen@temp@five\relax \endgroup - \kern\gre@dimen@nabc@elementadvance\relax - \global\gre@nabc@halign@pendingfalse - \global\gre@nabc@halign@pending@ifalse - \global\gre@nabc@halign@pending@iifalse + \global\csname gre@nabc@halign@pending@#1false\endcsname + % Clear global pending flag when no voice remains pending. + \ifgre@nabc@halign@pending@i\else + \ifgre@nabc@halign@pending@ii\else + \global\gre@nabc@halign@pendingfalse + \fi + \fi + \fi +}% + +% Flush all deferred center-aligned NABC voices. +\def\gre@nabc@flush@deferred@halign{% + \ifgre@nabc@halign@pending + \gre@nabc@flush@deferred@voice{i}% + \gre@nabc@flush@deferred@voice{ii}% \fi }% diff --git a/tex/gregoriotex-nabc.tex b/tex/gregoriotex-nabc.tex index ae46cb4d..f8a8d29d 100644 --- a/tex/gregoriotex-nabc.tex +++ b/tex/gregoriotex-nabc.tex @@ -152,6 +152,13 @@ \gre@nabc@halign@pending@ifalse \newif\ifgre@nabc@halign@pending@ii \gre@nabc@halign@pending@iifalse +% Per-voice advance start: the value of \gre@dimen@nabc@elementadvance +% at the moment each voice entered pending state. This allows each voice +% to span a different number of elements when one voice has empty NABC. +\newdimen\gre@dimen@nabc@advance@start@i +\gre@dimen@nabc@advance@start@i=0pt\relax +\newdimen\gre@dimen@nabc@advance@start@ii +\gre@dimen@nabc@advance@start@ii=0pt\relax \def\gresetnabc#1#2{% \gre@processvisibilitywithphantom{#2}% @@ -254,7 +261,13 @@ } \def\GreNABCNeumes#1#2#3#4{% - \gre@nabc@flush@deferred@halign + % Flush only the voice being replaced, not all voices. This allows + % a voice whose NABC is empty on the next element to keep accumulating + % advance and center over the combined span. + \ifcase#1\relax + \or \gre@nabc@flush@deferred@voice{i}% + \or \gre@nabc@flush@deferred@voice{ii}% + \fi \csname ifgre@nabcvoice@\romannumeral#1@visible\endcsname % \ifcase#1\relax \or % 1 From 171925542d2408b6a8cbba52d4118cdfd5f0ec23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?La=C3=A9rcio=20de=20Sousa?= Date: Thu, 5 Mar 2026 18:51:00 -0300 Subject: [PATCH 4/7] Fix per-voice flush: use temp@one to avoid clobber by @now raise The @now placement macros overwrite \gre@dimen@temp@five with the vertical raise amount. Using that same register for the horizontal span caused the kern-forward after placement to use the wrong value, breaking NABC line 2 (below-staff) positioning. Switch to \gre@dimen@temp@one which is not touched by @now. --- tex/gregoriotex-main.tex | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/tex/gregoriotex-main.tex b/tex/gregoriotex-main.tex index d68cc185..cfce36fb 100644 --- a/tex/gregoriotex-main.tex +++ b/tex/gregoriotex-main.tex @@ -797,15 +797,17 @@ \begingroup \gre@dimen@temp@five=\dimexpr\gre@dimen@nabc@elementadvance - \csname gre@dimen@nabc@advance@start@#1\endcsname\relax - \kern-\gre@dimen@temp@five\relax - \gre@dimen@lastglyphwidth=\gre@dimen@temp@five\relax +% N.B. We use \gre@dimen@temp@one for the span because the @now macros +% overwrite \gre@dimen@temp@five with the vertical raise amount. +\def\gre@nabc@flush@deferred@voice#1{% + \csname ifgre@nabc@halign@pending@#1\endcsname + \begingroup + \gre@dimen@temp@one=\dimexpr\gre@dimen@nabc@elementadvance + - \csname gre@dimen@nabc@advance@start@#1\endcsname\relax + \kern-\gre@dimen@temp@one\relax + \gre@dimen@lastglyphwidth=\gre@dimen@temp@one\relax \csname gre@nabc@place@voice@#1@now\endcsname - \kern\gre@dimen@temp@five\relax - \endgroup - \global\csname gre@nabc@halign@pending@#1false\endcsname - % Clear global pending flag when no voice remains pending. - \ifgre@nabc@halign@pending@i\else - \ifgre@nabc@halign@pending@ii\else + \kern\gre@dimen@temp@onng@ii\else \global\gre@nabc@halign@pendingfalse \fi \fi From 648a4a20ae588de9b24da6aa15732f7a52b8148a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?La=C3=A9rcio=20de=20Sousa?= Date: Sun, 8 Mar 2026 19:58:14 -0300 Subject: [PATCH 5/7] Flush deferred NABC at element boundaries When center-aligning NABC glyphs over elements (neume mode), voices that had no NABC on a subsequent element would remain pending across element boundaries. This caused their centering span to grow to include the inter-element space and the next element's width, resulting in NABC glyphs displaced far from their correct position. Fix: call \gre@nabc@flush@deferred@halign at the very beginning of \GreEndOfElement, before penalty/space insertion. This ensures each voice's centering span is bounded by its element's glyphs. Note: the \ifgre@nabc@halign@pending block that accumulated inter-element space into elementadvance is now dead code (since we flush before reaching it), but is kept for forward compatibility with the nabcinterglyphmingap branch, which replaces it with \gre@nabc@reduce@rightbalance. --- tex/gregoriotex-main.tex | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/tex/gregoriotex-main.tex b/tex/gregoriotex-main.tex index cfce36fb..c0393033 100644 --- a/tex/gregoriotex-main.tex +++ b/tex/gregoriotex-main.tex @@ -792,11 +792,6 @@ % #1 = voice suffix (i or ii). % Kerns back from current position to the voice's start, centers over % that voice's span, then kerns forward to the current position. -\def\gre@nabc@flush@deferred@voice#1{% - \csname ifgre@nabc@halign@pending@#1\endcsname - \begingroup - \gre@dimen@temp@five=\dimexpr\gre@dimen@nabc@elementadvance - - \csname gre@dimen@nabc@advance@start@#1\endcsname\relax % N.B. We use \gre@dimen@temp@one for the span because the @now macros % overwrite \gre@dimen@temp@five with the vertical raise amount. \def\gre@nabc@flush@deferred@voice#1{% @@ -807,7 +802,12 @@ \kern-\gre@dimen@temp@one\relax \gre@dimen@lastglyphwidth=\gre@dimen@temp@one\relax \csname gre@nabc@place@voice@#1@now\endcsname - \kern\gre@dimen@temp@onng@ii\else + \kern\gre@dimen@temp@one\relax + \endgroup + \global\csname gre@nabc@halign@pending@#1false\endcsname + % Clear global pending flag when no voice remains pending. + \ifgre@nabc@halign@pending@i\else + \ifgre@nabc@halign@pending@ii\else \global\gre@nabc@halign@pendingfalse \fi \fi @@ -1555,6 +1555,9 @@ %% 2: unison (breakable according to the unisonbreakbehavior setting) % #3 is the number of notes emitted in this syllable before this macro \def\GreEndOfElement#1#2#3{% + % Flush all deferred NABC voices at element boundaries so that centering + % spans do not extend across elements. + \gre@nabc@flush@deferred@halign \ifnum\gre@count@syllablenotes<\gre@count@unbreakabletotalnotes\relax % \gre@unbreakableendofelementtrue % \else % From 9363d02e0ae50e4b794b104d9acea0e8d5e95489 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?La=C3=A9rcio=20de=20Sousa?= Date: Tue, 10 Mar 2026 17:27:34 -0300 Subject: [PATCH 6/7] Fix centering overflow for combined NABC glyphs When font resolution combines an LS (significative letter) with the base neume into a single combined glyph (e.g. 'pulsnt3' in grelaon), the post-resolution lwidths loop finds ls[i]='' and accumulates zero width. This caused lwidths[12] (right overflow) to be 0 even when the neume has right-side LS, making the centering formula in neume mode center the entire glyph (including LS) instead of just the neume part. Fix: compute LS widths from the original ls[] entries BEFORE font resolution clears them for combined glyphs. Use these pre-resolution widths (overflow_left, overflow_right) for the alignment overflow dimensions passed to TeX, while keeping the post-resolution lwidths for add_ls positioning of separately-rendered LS parts. --- tex/gregoriotex-nabc.lua | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/tex/gregoriotex-nabc.lua b/tex/gregoriotex-nabc.lua index bf042226..7316eab6 100644 --- a/tex/gregoriotex-nabc.lua +++ b/tex/gregoriotex-nabc.lua @@ -336,6 +336,20 @@ local gregallparse_neumes = function(str, kind, scale, voice) end lscount = lscount + 1 end + -- Accumulate per-position LS widths from ALL original entries, + -- before font resolution may clear ls[i] for combined glyphs. + -- This ensures overflow widths (lwidths[10]/[12]) are correct + -- even when LS are baked into a combined font glyph. + local all_lwidths = { 0, 0, 0, 0, 0, 0, 0, 0, 0 } + for i = 0, lscount - 1 do + if ls[i] ~= '' then + local p = tonumber(ls[i]:sub(-1, -1)) + local l = ls[i]:sub(1, -2) + if gregallmetrics[kind][l] then + all_lwidths[p] = all_lwidths[p] + gregallmetrics[kind][l].width + end + end + end if base ~= "ERR" then local l = {} function l.try (kind, base, parts, pp, su, ls5, ls) @@ -445,6 +459,10 @@ local gregallparse_neumes = function(str, kind, scale, voice) lwidths[10] = math.max (lwidths[1], lwidths[4], lwidths[7]) lwidths[11] = math.max (lwidths[2], lwidths[8]) lwidths[12] = math.max (lwidths[3], lwidths[6], lwidths[9]) + -- For alignment overflow purposes, use the pre-resolution widths + -- so that LS baked into combined glyphs are still accounted for. + local overflow_left = math.max(all_lwidths[1], all_lwidths[4], all_lwidths[7]) + local overflow_right = math.max(all_lwidths[3], all_lwidths[6], all_lwidths[9]) local pre = '' local post = '' for i = 0, lscount - 1 do @@ -463,8 +481,8 @@ local gregallparse_neumes = function(str, kind, scale, voice) -- reserve space at the note level and prevent overlap with the -- previous element. local is_neume_mode = (get_nabc_alignment(voice) == 'neume') - if is_neume_mode and lwidths[10] > 0 then - local overflow_sp = string.format("%.3f", lwidths[10] * scale) + if is_neume_mode and overflow_left > 0 then + local overflow_sp = string.format("%.3f", overflow_left * scale) base = '\\global\\gre@dimen@nabcleftoverflow=' .. overflow_sp .. 'sp' .. '\\kern -' .. overflow_sp .. 'sp' .. base end @@ -474,9 +492,10 @@ local gregallparse_neumes = function(str, kind, scale, voice) -- group, mirroring the left-side exclusion. local rdim = '\\gre@dimen@nabcrightoverflow@' .. (voice == 1 and 'i' or 'ii') - if is_neume_mode and lwidths[12] > 0 then + if is_neume_mode and overflow_right > 0 then + local rval = string.format("%.3f", overflow_right * scale) base = base .. '\\global' .. rdim .. '=' - .. string.format("%.3f", lwidths[12] * scale) .. 'sp' + .. rval .. 'sp' else base = base .. '\\global' .. rdim .. '=0sp' end From 734fa4a8adcba68686a835cd990cfda92997a8c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?La=C3=A9rcio=20de=20Sousa?= Date: Thu, 12 Mar 2026 09:19:24 -0300 Subject: [PATCH 7/7] fix(nabc): prevent line break while deferred center NABC is pending When center-aligned NABC placement is deferred across element boundaries, a line break at GreEndOfElement can move pending NABC rendering to the next line, visually detaching neumes from their corresponding GABC notes (e.g. ves(...hh|ds1|bv...)). Force end-of-element to be unbreakable while NABC halign is pending so the deferred flush stays on the same line as its glyph context. --- tex/gregoriotex-main.tex | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tex/gregoriotex-main.tex b/tex/gregoriotex-main.tex index c0393033..0bf34de1 100644 --- a/tex/gregoriotex-main.tex +++ b/tex/gregoriotex-main.tex @@ -1580,6 +1580,11 @@ \fi % \fi % \fi % + % While center-aligned NABC is deferred, keep elements unbreakable so + % pending NABC cannot be flushed on the next line detached from its glyph. + \ifgre@nabc@halign@pending + \gre@unbreakableendofelementtrue % + \fi \ifgre@unbreakableendofelement % \GreNoBreak % \else %