Como posso melhorar o espaçamento após uma flutuação superior?

Como posso melhorar o espaçamento após uma flutuação superior?

Contexto

Estou tentando criar uma macro (chamada aqui \myCommand) que separe dois parágrafos e adicione um espaço vertical diferente dependendo do contexto. Mas suponho que este seja um problema geral que também pode ocorrer com macros como \section{...}.

Normalmente, eu gostaria de inserir um espaço de valor \myLength(para fins deste exemplo, usaremos \myLength= 30 pt) no início \myCommande no final dele.
Se a macro cair no topo da página, após uma quebra de página, \vspace{\myLength}é ignorada e nenhum espaço vertical é adicionado, que é o que desejo.
MAS o problema surge quando, como no caso anterior, a macro cai após uma quebra de página e é precedida por um ou mais pontos flutuantes superiores. Nesse caso, gostaria que a macro inserisse uma vertical \vspace*{\dimexpr\myLength-\textfloatsep}para manter a consistência.


Exemplo

Aqui está um exemplo para ilustrar:

Abaixo, a macro se comporta corretamente: quando cai no topo da página após uma quebra de página, nenhum espaço é inserido no início da macro; e quando não está no topo da página, \vspace{\myLength}está inserido corretamente no início da macro.

insira a descrição da imagem aqui

Porém, quando a macro cai no topo da página após uma quebra de página e é precedida por um float superior, os espaços ficam totalmente desequilibrados.

insira a descrição da imagem aqui

Apenas para mostrar que o problema é geral e não específico \myCommand, aqui está o mesmo problema com \section{...}:

insira a descrição da imagem aqui

Aqui está um exemplo mínimo que não 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 o problema mencionado acima, gostaria de um espaço vertical com valor de \vspace*{\dimexpr\myLength-\textfloatsep}(para compensar o espaço vertical \textfloatsepinserido pelo float superior) apenas no seguinte caso:
no início de uma página (após uma quebra de página) E a o flutuador superior está presente.

Em todos os outros casos, using \vspace{\myLength}funciona perfeitamente.


Ensaios

Minha abordagem (até agora sem sucesso) foi usar duas condicionais para inserir os espaços verticais corretos no lugar certo:

  • Uma primeira condicional para testar a presença de um float superior na página atual,
  • Uma segunda condicional para verificar se estamos no topo de uma nova página, após uma quebra de página.

Se ambas as condições forem atendidas, então estamos de fato no início de uma página (após uma quebra de página) E um float superior está presente; então inserimos \vspace*{\dimexpr\myLength-\textfloatsep}. Em todos os outros casos, inserimos \vspace{\myLength}.

Graças a uma pergunta anterior, consegui encontrar uma maneira de detectar a presença de um ponto flutuante no topo da página em uma página:Como determinar no corpo do documento se uma página tem ou não um flutuador superior?.

A segunda condicional representa um problema para mim, entretanto, e eu queria tentar usar \pagetotal, mas não parece adequado:Como acessar overdadeirovalor de \pagetotal?.

Minha macro \myCommandentão ficou assim:

\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

Minha abordagem certamente não é a correta.

Responder1

Aqui está uma solução baseada em LuaTeX. Qualquer coisa dentro do comando \removefromtopserá removida se cair no topo da página e mantida caso contrário. Estou usando uma regra aqui para tornar o efeito mais óbvio, mas você pode substituí-la por um \vspace(ou qualquer outro material de modo vertical) para o 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}

saída

O código é bastante complexo e não o testei muito, então esperaria alguns bugs em documentos mais complicados.

Como funciona

Primeiro, definimos um “atributo” personalizado que podemos usar para marcar o conteúdo de \removefromtop.

Em seguida, nos conectamos ao buildpage_filter, que é chamado logo antes do TeX adicionar conteúdo à “lista vertical principal”. A parte complicada aqui é que chamamostex.triggerbuildpage() dentroesse retorno de chamada, que faz com que o TeX libere suas “contribuições recentes” e tente uma quebra de página.

Se uma quebra de página for acionada, o controle saltará para o arquivo pre_output_filter. Aqui, adicionamos outro retorno de chamada que primeiro verifica se esta é uma quebra de página “real” (e não uma quebra de página falsa usada pelo LaTeX para liberar carros flutuantes) e se não há carros flutuantes superiores na página atual. Se for esse o caso, iteramos pelo conteúdo da página e removemos qualquer conteúdo inicial marcado com nosso atributo personalizado. E se marcamos algum conteúdo, sinalizamos isso de volta para a nossa página buildpage_filtere retornamos o novo conteúdo da página.

Assim que pre_output_filterterminar, o controle será retomado após o tex.triggerbuildpage(), exceto por enquanto, estamos dentro da rotina de saída. Se o mais recente pre_output_filterremoveu algum conteúdo com sucesso e estamos dentro da primeira rotina de saída, removemos todos os tokens da pilha de entrada do TeX. Como estamos dentro da rotina de saída, isso esvazia a expansão atual da \outputlista de tokens. Em seguida, limpamos \box255(o conteúdo da página atual), movemos seu conteúdo de volta para a lista de contribuições recentes e retornamos.

Por que fazer assim?

Não podemos saber se a \removefromtopcaixa está ou não no topo da página até que a rotina de saída seja acionada. Porém, quando removemos as \removefromtopcaixas, encurtamos a página, então se apenas engancharmos pre_output_filterpara remover as caixas, todas as páginas ficarão muito curtas.

Em vez disso, deixamos a rotina de saída disparar normalmente e, se removermos qualquer uma das \removefromtopcaixas, colocamos o conteúdo da página atual de volta na lista de contribuições recentes, cancelamos a rotina de saída e deixamos o TeX prosseguir normalmente para adicionar mais conteúdo ao Rodapé da página.

Responder2

Acho que o problema está no espaço vertical adicionado após o flutuador. Então, na minha sugestão abaixo, eu modifico isso usando \myLengthe adiciono um \baselineskiptambém.

\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}

Responder3

Depois de vários dias de pesquisa, acho que encontrei uma solução para o meu problema. A ideia é a seguinte:

Na rotina de saída, o \@cfltcomando monta, quando apropriado, os flutuadores superiores com o corpo do texto. Modifiquei este comando para registrar no arquivo auxiliar a coordenada Y que representa o início do corpo do texto (ou seja, logo após qualquer flutuação no topo da página). Quando executo \myCommand, faço a mesma coisa e salvo a coordenada Y que representa o início da macro no arquivo auxiliar.

Na próxima compilação, comparo as duas coordenadas Y. Se forem iguais, então \myCommandfica logo após os flutuadores superiores, e posso inserir \vspace*{\dimexpr\myLength-\textfloatsep}conforme solicitado na pergunta.

O resultado é o seguinte:

insira a descrição da imagem aqui

que é mais harmonioso que o resultado original (embora possam ser feitos pequenos ajustes):

insira a descrição da imagem aqui


Durante meus testes, porém, encontrei um problema inerente ao aplicativo. Na verdade, o espaço inserido pela macro depende da posição da macro na página, que por sua vez depende do espaço inserido pela macro, etc. Existe uma dependência circular. Para evitar isso, suponho que você teria que evitar uma quebra de página antes ou depois, \myCommanddependendo de como deseja usá-la. Estou aberto a sugestões de melhorias.

Por exemplo, no meu MWE, se para o meu 4º parágrafo eu usar \lipsum[2]em vez de \lipsum[3], ocorre instabilidade e a compilação dos documentos dá sucessivamente os casos A (esquerda) e B (direita) abaixo:

insira a descrição da imagem aquiinsira a descrição da imagem aqui

Se começarmos com o caso A, a macro terminará no topo do corpo do texto na página 2.
Portanto, na próxima compilação, inseriremos um espaço igual ao \myLength-\textfloatsepinício de \myCommand. Agora, o espaço vertical \myLength-\textfloatsepé menor que o espaço vertical \myLength, então há espaço suficiente para o comando estar na página 1 e estamos no caso B.
Na próxima compilação, como não estamos mais atrás de um float superior, \myCommandinsere um espaço vertical igual a \myLength. Desta vez, não há espaço suficiente na página 1 e \myCommandé enviado de volta ao início da página 2, então voltamos ao caso A.


O 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}

informação relacionada