¿Cómo puedo mejorar el espacio después de una flotación superior?

¿Cómo puedo mejorar el espacio después de una flotación superior?

Contexto

Estoy intentando crear una macro (llamada aquí \myCommand) que separa dos párrafos y agrega un espacio vertical diferente según el contexto. Pero supongo que este es un problema general que también puede ocurrir con macros como \section{...}.

Normalmente, me gustaría insertar un espacio de valor \myLength(para los propósitos de este ejemplo, tomaremos \myLength= 30 pt) al principio \myCommandy al final.
Si la macro cae en la parte superior de la página, después de un salto de página, \vspace{\myLength}se ignora y no se agrega ningún espacio vertical, que es lo que quiero.
PERO el problema surge cuando, como en el caso anterior, la macro cae después de un salto de página y está precedida por uno o más flotadores superiores. En este caso, me gustaría que la macro insertara una vertical \vspace*{\dimexpr\myLength-\textfloatsep}para mantener la coherencia.


Ejemplo

Aquí hay un ejemplo para ilustrar:

A continuación, la macro se comporta correctamente: cuando cae en la parte superior de la página después de un salto de página, no se inserta ningún espacio al inicio de la macro; y cuando no está en la parte superior de la página, \vspace{\myLength}se inserta correctamente al inicio de la macro.

ingrese la descripción de la imagen aquí

Sin embargo, cuando la macro cae en la parte superior de la página después de un salto de página y está precedida por un flotador superior, los espacios están totalmente desequilibrados.

ingrese la descripción de la imagen aquí

Solo para mostrar que el problema es general y no específico \myCommand, aquí está el mismo problema con \section{...}:

ingrese la descripción de la imagen aquí

Aquí hay un ejemplo mínimo que no funciona:

\documentclass{article}
\usepackage{lipsum, mwe}

\newlength{\myLength}
\setlength{\myLength}{30pt}% exaggerated value to illustrate

\newcommand{\myCommand}{\par
    \vspace{\myLength}
    {\centering\LARGE * * *\par}
    \vspace{\myLength}
}

\begin{document}
\lipsum[1]

\lipsum[1]

\lipsum[1]

\lipsum[2]

\begin{figure}[t]
\centering
\includegraphics{example-image}
\end{figure}

\myCommand

\lipsum[1]

\lipsum[2]

\myCommand

\lipsum[1]

\myCommand

\lipsum[1]
\end{document}

Meta

Para superar el problema mencionado anteriormente, me gustaría un espacio vertical con un valor de \vspace*{\dimexpr\myLength-\textfloatsep}(para compensar el espacio vertical \textfloatsepinsertado por el flotador superior) solo en el siguiente caso:
al inicio de una página (después de un salto de página) Y un El flotador superior está presente.

En todos los demás casos, el uso \vspace{\myLength}funciona perfectamente.


Ensayos

Mi enfoque (hasta ahora infructuoso) ha sido utilizar dos condicionales para insertar los espacios verticales correctos en el lugar correcto:

  • Un primer condicional para probar la presencia de un flotador superior en la página actual,
  • Un segundo condicional para comprobar que estamos en la parte superior de una nueva página, después de un salto de página.

Si se cumplen ambas condiciones, entonces estamos efectivamente al comienzo de una página (después de un salto de página) Y hay un flotador superior presente; luego insertamos \vspace*{\dimexpr\myLength-\textfloatsep}. En todos los demás casos, insertamos \vspace{\myLength}.

Gracias a una pregunta anterior, pude encontrar una manera de detectar la presencia de un flotador en la parte superior de una página:¿Cómo determinar a partir del cuerpo del documento si una página tiene o no un flotador superior?.

Sin embargo, el segundo condicional me plantea un problema y quería intentar usarlo \pagetotal, pero no me parece adecuado:¿Cómo acceder alverdaderovalor de \pagetotal?.

Mi macro \myCommandentonces se veía así:

\makeatletter
\usepackage{refcount}
%From : https://tex.stackexchange.com/questions/712713/how-to-determine-from-the-document-body-whether-or-not-a-page-has-a-top-float
\def \@combinefloats {%
    \ifx \@toplist\@empty%
    \else%
        \@cflt%
        \immediate\write\@auxout{\string\global\string\@namedef{pageWithTopFloat-\thepage}{1}}%
    \fi%
    \ifx \@botlist\@empty \else \@cflb \fi%
}

\newcounter{ifTopFloatCnt}

\long\def\ifTopFloat#1#2{%
    \global\advance\c@ifTopFloatCnt\@ne%
    \label{\the\c@ifTopFloatCnt @ifTopFloat}\nopagebreak%
    \ifcsname pageWithTopFloat-\getpagerefnumber{\the\c@ifTopFloatCnt @ifTopFloat}\endcsname%
        #1%
    \else%
        #2%
    \fi%
}

\newlength{\myLength}
\setlength{\myLength}{30pt}% exaggerated value to illustrate

\newcommand{\mycommand}{\par%
    \ifTopFloat{%
        \ifdim\pagetotal=0pt% If there is a top float AND you are at the start of a new page
            \vspace*{\dimexpr\myLength-\textfloatsep}
            {\centering\LARGE * * *\par}
        \else% If there is a top float BUT you are not at the start of a new page
            \vspace{\myLength}
            {\centering\LARGE * * *\par}
        \fi
    }{% If there is no top float
        \vspace{\myLength}
        {\centering\LARGE * * *\par}
    }    
    \vspace{\myLength}
}
\makeatother

Ciertamente mi enfoque no es el correcto.

Respuesta1

Aquí hay una solución basada en LuaTeX. Todo lo que esté dentro del comando \removefromtopse eliminará si se encuentra en la parte superior de la página y se mantendrá en caso contrario. Estoy usando una regla aquí para hacer que el efecto sea más obvio, pero puedes reemplazarla con \vspace(o cualquier otro material en modo vertical) para el documento final.

\documentclass{article}

\usepackage{luacode}
\begin{luacode*}
    local attr = luatexbase.new_attribute("removefromtop")
    token.set_char("removefromtopattr", attr, "global")

    local cancel_output = false
    local last_cancelled = false

    luatexbase.add_to_callback("pre_output_filter", function (head)
        local outputpenalty = tex.outputpenalty
        if outputpenalty <= -10002 and outputpenalty >= -10005 then
            return true
        end

        if token.get_macro("@toplist") ~= "" then
            return true
        end

        for n in node.traverse(head) do
            if node.get_attribute(n, attr) == 1 then
                head = node.remove(head, n)
                cancel_output = true
            elseif n.id ~= node.id("penalty") and
                   n.id ~= node.id("glue")    and
                   n.id ~= node.id("kern")    and
                   n.id ~= node.id("mark")    and
                   n.id ~= node.id("whatsit") and
                   n.id ~= node.id("ins")
            then
                break
            end
        end

        if last_cancelled then
            last_cancelled = false
            for n in node.traverse_id(node.id("glue"), head) do
                if n.subtype == 10 then
                    n.width = -tex.baselineskip.width
                end
            end
        end

        if cancel_output then
            return head
        else
            return true
        end
    end, "remove_from_top")

    luatexbase.add_to_callback("buildpage_filter", function (info)
        if status.output_active then
            return
        end

        if not tex.output:match("%}%}") then
            tex.runtoks(function()
                tex.sprint[[\output\expandafter{\the\output}]]
            end)
        end

        tex.triggerbuildpage()

        if not (status.output_active and cancel_output) then
            return
        end

        cancel_output = false
        last_cancelled = true

        local level = 0
        repeat
            local t = token.get_next()
            if t.command == 1 then
                level = level + 1
            elseif t.command == 2 then
                level = level - 1
            end
        until level == 0

        local box = node.copy_list(tex.box[255])
        node.write(box)
        tex.setbox("global", 255, nil)
    end, "remove_from_top")
\end{luacode*}

\newcommand{\removefromtop}[1]{%
    \vbox attr \removefromtopattr=1 {%
        #1%
    }%
}

\usepackage{lipsum, mwe}

\newlength{\myLength}
\setlength{\myLength}{20pt}% exaggerated value to illustrate

\newcommand{\myCommand}{
    \removefromtop{
        \hrule height \myLength width \textwidth depth 0pt
    }
    \nobreak
    \vbox{
        {\centering\LARGE * * *\par}
        \hrule height \myLength width \textwidth depth 0pt
    }
}

\begin{document}
\lipsum[1]

\lipsum[1]

\lipsum[1]

\lipsum[2]

\begin{figure}[t]
\centering
\includegraphics{example-image}
\end{figure}

\myCommand

\lipsum[1]

\lipsum[2]

\myCommand

\lipsum[1]

\myCommand

\lipsum[1]

\lipsum[2]

\myCommand

\lipsum[1]

\myCommand

\lipsum[1-3]

\myCommand

\lipsum[1-9]

\myCommand

\lipsum[1]

\begin{figure}[t]
    \centering
    \includegraphics{example-image}
\end{figure}

\end{document}

producción

El código es bastante complejo y no lo he probado mucho, por lo que esperaría algunos errores con documentos más complicados.

Cómo funciona

Primero, definimos un "atributo" personalizado que podemos usar para marcar el contenido de \removefromtop.

A continuación, nos conectamos al buildpage_filter, que se llama justo antes de que TeX agregue contenido a la "lista vertical principal". Lo complicado aquí es que llamamostex.triggerbuildpage() adentroesta devolución de llamada, lo que hace que TeX borre sus “contribuciones recientes” e intente un salto de página.

Si se activa un salto de página, el control salta al archivo pre_output_filter. Aquí, hemos agregado otra devolución de llamada que primero verifica si se trata de un salto de página "real" (y no un salto de página falso utilizado por LaTeX para vaciar los flotantes) y que no hay flotantes superiores en la página actual. Si este es el caso, recorremos el contenido de la página y eliminamos cualquier contenido inicial marcado con nuestro atributo personalizado. Y si marcamos algún contenido, lo indicamos a nuestro buildpage_filtery devolvemos el contenido de la nueva página.

Una vez que pre_output_filterfinaliza, el control se reanuda después de tex.triggerbuildpage(), excepto que por ahora estamos dentro de la rutina de salida. Si el más reciente pre_output_filtereliminó con éxito algún contenido y estamos dentro de la primera rutina de salida, entonces eliminamos todos los tokens en la pila de entrada de TeX. Como estamos dentro de la rutina de salida, esto vacía la expansión actual de la \outputlista de tokens. Luego limpiamos \box255(el contenido de la página actual), volvemos a mover su contenido a la lista de contribuciones recientes y regresamos.

¿Por qué hacerlo de esta manera?

No podemos saber si el \removefromtopcuadro está en la parte superior de la página o no hasta que se active la rutina de salida. Sin embargo, cuando eliminamos los \removefromtopcuadros, acortamos la página, por lo que si simplemente enganchamos pre_output_filterpara eliminar los cuadros, todas las páginas serán demasiado cortas.

En su lugar, dejamos que la rutina de salida se active normalmente, luego, si eliminamos cualquiera de las \removefromtopcasillas, volvemos a colocar el contenido de la página actual en la lista de contribuciones recientes, cancelamos la rutina de salida y dejamos que TeX proceda normalmente para agregar más contenido a la lista. final de la página.

Respuesta2

Creo que el problema está en el espacio vertical agregado después del flotador. Entonces, en mi sugerencia a continuación, modifico ese uso \myLengthy agrego un \baselineskiptambién.

\documentclass{article}
\usepackage{lipsum, mwe}

\newlength{\myLength}
\setlength{\myLength}{30pt}% exaggerated value to illustrate

% Below, I increase the separation between float and text and reduce
% the flexibility of the space
% You can see the default values by adding \the\textfloatsep inside
% the document environment
\newlength{\myFloatskip}
\setlength{\myFloatskip}{\the\myLength}
\addtolength{\myFloatskip}{\baselineskip}
\setlength{\textfloatsep}{\the\myFloatskip plus 0.0pt minus 0.0pt}

\newcommand{\myCommand}{\par%
    \vspace{\myLength}%
    {\centering\LARGE * * *\par}%
    \vspace{\myLength}%
}

\begin{document}

\lipsum[1]

\lipsum[1]

\lipsum[1]

\lipsum[2]

\begin{figure}[t]
\centering
\includegraphics{example-image}
\end{figure}

\myCommand

\lipsum[2]

\lipsum[3]

\myCommand

\lipsum[1]

\myCommand

\lipsum[1]
\end{document}

Respuesta3

Después de varios días de investigación, creo que encontré una solución a mi problema. La idea es la siguiente:

En la rutina de salida, el \@cfltcomando ensambla, en su caso, los flotadores superiores con el texto del cuerpo. Modifiqué este comando para registrar en el archivo auxiliar la coordenada Y que representa el inicio del cuerpo del texto (es decir, justo después de cualquier elemento flotante en la parte superior de la página). Cuando ejecuto \myCommand, hago lo mismo y guardo la coordenada Y que representa el inicio de la macro en el archivo auxiliar.

En la siguiente compilación, comparo las dos coordenadas Y. Si son iguales, entonces \myCommandestá justo después de los flotadores superiores y puedo insertarlos \vspace*{\dimexpr\myLength-\textfloatsep}según lo solicitado en la pregunta.

El resultado es el siguiente:

ingrese la descripción de la imagen aquí

que es más armonioso que el resultado original (aunque se podrían hacer pequeños ajustes):

ingrese la descripción de la imagen aquí


Sin embargo, durante mis pruebas encontré un problema inherente a la aplicación. En efecto, el espacio insertado por la macro depende de la posición de la macro en la página, que a su vez depende del espacio insertado por la macro, etc. Hay una dependencia circular. Para evitar esto, supongo que deberías evitar un salto de página antes o después \myCommanddependiendo de cómo quieras usarlo. Estoy abierto a sugerencias de mejora.

Por ejemplo, en mi MWE, si para mi cuarto párrafo uso \lipsum[2]en lugar de \lipsum[3], se produce inestabilidad y la compilación de los documentos da sucesivamente los casos A (izquierda) y B (derecha) a continuación:

ingrese la descripción de la imagen aquíingrese la descripción de la imagen aquí

Si comenzamos con el caso A, la macro termina en la parte superior del cuerpo del texto en la página 2.
Entonces, en la próxima compilación, insertaremos un espacio igual \myLength-\textfloatsepal comienzo de \myCommand. Ahora, el espacio vertical \myLength-\textfloatsepes menor que el espacio vertical \myLength, por lo que hay suficiente espacio para que el comando esté en la página 1 y estamos en el caso B.
En la próxima compilación, como ya no buscamos un flotador superior, \myCommandinserta un espacio vertical igual a \myLength. Esta vez, no hay suficiente espacio en la página 1 y \myCommandse devuelve al principio de la página 2, por lo que volvemos al caso A.


El MWE:

\documentclass{article}
\usepackage{lipsum, mwe, refcount, iftex}

\ifluatex% For compatibility with LuaTeX, which I generally use.
    \let\pdfsavepos\savepos
    \let\pdflastypos\lastypos
\fi

\makeatletter
\def \@cflt{%
    \let \@elt \@comflelt
    \setbox\@tempboxa \vbox{}%
    \@toplist
    \setbox\@outputbox \vbox{%
        \boxmaxdepth \maxdepth
        \unvbox\@tempboxa
        \vskip -\floatsep
        \topfigrule
        \vskip \textfloatsep
        \pdfsavepos% <--- Added
        \write\@auxout{\string\global\string\@namedef{@pageWithTopFloatYPos-\thepage}{\the\pdflastypos}}% <--- Added. We save the Y position of the top of "\@outputbox", i. e. the body of the text.
        \unvbox\@outputbox
    }%
    \let\@elt\relax
    \xdef\@freelist{\@freelist\@toplist}%
    \global\let\@toplist\@empty
}

\newcounter{@ifTopFloatCnt}

\long\def\ifTopFloat#1#2{% A conditional to see if there is a top float on the current page. See https://tex.stackexchange.com/questions/712713/how-to-determine-from-the-document-body-whether-or-not-a-page-has-a-top-float.
    \stepcounter{@ifTopFloatCnt}
    \label{@ifTopFloat-\the@ifTopFloatCnt}\nopagebreak%
    \ifcsname @pageWithTopFloatYPos-\getpagerefnumber{@ifTopFloat-\the@ifTopFloatCnt}\endcsname%
        #1%
    \else%
        #2%
    \fi%
}

\newlength{\myLength}
\setlength{\myLength}{30pt}% exaggerated value to illustrate

\newcounter{@myCommandCnt}
\newcounter{@myCommandCntAux}

\newcommand{\myCommand}{\par%
    \stepcounter{@myCommandCnt}%
    \pdfsavepos%
    \write\@auxout{%
        \string\stepcounter{@myCommandCntAux}%
        ^^J% New line in the auxiliary file
        \string\global\string\@namedef{@myCommandYPos-\string\the@myCommandCntAux}{\the\pdflastypos}% Save the Y coordinate.
    }%
    \ifTopFloat{%
        \ifnum \@nameuse{@myCommandYPos-\the@myCommandCnt} = 
            \csname @pageWithTopFloatYPos-\getpagerefnumber{@ifTopFloat-\the@ifTopFloatCnt}\endcsname% If there's a top float AND you're right after it
            \vspace*{\dimexpr\myLength-\textfloatsep}
            {\centering\LARGE * * * (a)\par}
        \else% If there is a top float BUT you are not right after it
            \vspace{\myLength}
            {\centering\LARGE * * * (b)\par}
        \fi
    }{% If there is no top float
        \vspace{\myLength}
        {\centering\LARGE * * * (c)\par}
    }    
    \vspace{\myLength}
}
\makeatother

\begin{document}
\lipsum[1]

\lipsum[1]

\lipsum[1]

\lipsum[3]% <--- If the current line is uncommented and the next line is commented, stability is achieved.
%\lipsum[2]% <--- If the previous line is commented and the current line is uncommented, instability occurs.

\begin{figure}[t]
\centering
\includegraphics{example-image}
\end{figure}

\myCommand

\lipsum[1]

\lipsum[2]

\begin{figure}[t]
\centering
\includegraphics{example-image-9x16}
\end{figure}

\myCommand

\lipsum[2]

\myCommand

\lipsum[1]

\myCommand

\lipsum[1]
\end{document}

información relacionada