Как можно улучшить интервал после верхнего флоатинга?

Как можно улучшить интервал после верхнего флоатинга?

Контекст

Я пытаюсь создать макрос (называемый здесь \myCommand), который разделяет два абзаца и добавляет разный вертикальный интервал в зависимости от контекста. Но я предполагаю, что это общая проблема, которая может также возникнуть с такими макросами, как \section{...}.

Обычно я хотел бы вставить пробел со значением \myLength(для целей этого примера мы возьмем \myLength= 30 pt) в начале \myCommandи в конце.
Если макрос попадает в верхнюю часть страницы, после разрыва страницы, \vspace{\myLength}он игнорируется и вертикальный пробел не добавляется, что мне и нужно.
НО проблема возникает, когда, как в предыдущем случае, макрос попадает после разрыва страницы и ему предшествует один или несколько верхних float. В этом случае я хотел бы, чтобы макрос вставлял вертикальный пробел \vspace*{\dimexpr\myLength-\textfloatsep}для сохранения согласованности.


Пример

Вот пример для иллюстрации:

Ниже макрос ведет себя правильно: когда он попадает в верхнюю часть страницы после разрыва страницы, в начале макроса не вставляется пробел; а когда он не находится в верхней части страницы, \vspace{\myLength}он правильно вставляется в начало макроса.

введите описание изображения здесь

Однако когда макрос располагается в верхней части страницы после разрыва страницы и ему предшествует верхний плавающий элемент, пробелы становятся совершенно несбалансированными.

введите описание изображения здесь

Чтобы показать, что проблема общая и не специфична для \myCommand, вот та же проблема с \section{...}:

введите описание изображения здесь

Вот нерабочий минимальный пример:

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

Цель

Чтобы решить указанную выше проблему, мне бы хотелось использовать вертикальный отступ со значением \vspace*{\dimexpr\myLength-\textfloatsep}(для компенсации вертикального отступа \textfloatsep, вставленного верхним плавающим элементом) только в следующем случае:
в начале страницы (после разрыва страницы) И присутствует верхний плавающий элемент.

Во всех остальных случаях использование \vspace{\myLength}работает отлично.


Испытания

Мой (пока безуспешный) подход заключался в использовании двух условных операторов для вставки нужных вертикальных пробелов в нужных местах:

  • Первое условие для проверки наличия верхнего плавающего элемента на текущей странице,
  • Второе условие для проверки того, что мы находимся в верхней части новой страницы после разрыва страницы.

Если оба условия выполнены, то мы действительно находимся в начале страницы (после разрыва страницы) И присутствует верхний плавающий элемент; тогда мы вставляем \vspace*{\dimexpr\myLength-\textfloatsep}. Во всех остальных случаях мы вставляем \vspace{\myLength}.

Благодаря предыдущему вопросу мне удалось найти способ обнаружения наличия плавающего элемента в верхней части страницы:Как определить по тексту документа, есть ли на странице верхний обтекатель?.

Однако второе условное предложение представляет для меня проблему, и я хотел попробовать использовать\pagetotal , но оно не показалось мне подходящим:Как получить доступистинныйзначение \pagetotal?.

Мой макрос \myCommandтогда выглядел так:

\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

Мой подход, безусловно, неверный.

решение1

Вот решение на основе LuaTeX. Все, что находится внутри команды, \removefromtopбудет удалено, если оно попадает в верхнюю часть страницы, и сохранено в противном случае. Я использую здесь правило, чтобы сделать эффект более очевидным, но вы можете заменить его на \vspace(или любой другой материал вертикального режима) для финального документа.

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

выход

Код довольно сложный, и я не особо его тестировал, поэтому я ожидаю возникновения ошибок в более сложных документах.

Как это работает

Сначала мы определяем пользовательский «атрибут», который можно использовать для маркировки содержимого \removefromtop.

Далее мы подключаемся к buildpage_filter, который вызывается непосредственно перед тем, как TeX добавляет содержимое в «главный вертикальный список». Хитрость здесь в том, что мы вызываемtex.triggerbuildpage() внутриэтот обратный вызов заставляет TeX очистить свои «последние вклады» и попытаться выполнить разрыв страницы.

Если срабатывает разрыв страницы, то управление переходит к pre_output_filter. Здесь мы добавили еще один обратный вызов, который сначала проверяет, является ли это «настоящим» разрывом страницы (а не поддельным разрывом страницы, используемым LaTeX для очистки плавающих элементов) и нет ли верхних плавающих элементов на текущей странице. Если это так, мы проходим по содержимому страницы и удаляем все начальное содержимое, отмеченное нашим пользовательским атрибутом. И если мы отметили какое-либо содержимое, то мы передаем это обратно в наш buildpage_filterи возвращаем новое содержимое страницы.

После pre_output_filterзавершения управление возобновляется после tex.triggerbuildpage(), за исключением того, что сейчас мы находимся внутри процедуры вывода. Если последняя pre_output_filterуспешно удалила некоторое содержимое, и мы находимся внутри первой процедуры вывода, мы удаляем все токены из входного стека TeX. Поскольку мы находимся внутри процедуры вывода, это очищает текущее расширение списка токенов \output. Затем мы очищаем \box255(содержимое текущей страницы), перемещаем ее содержимое обратно в список последних вкладов и возвращаемся.

Почему это так?

Мы не можем знать, \removefromtopнаходится ли блок в верхней части страницы или нет, пока не будет запущена процедура вывода. Однако, когда мы удаляем блоки \removefromtop, мы укорачиваем страницу, поэтому если бы мы просто подключили , pre_output_filterчтобы удалить блоки, то все страницы были бы слишком короткими.

Вместо этого мы позволяем процедуре вывода срабатывать как обычно, а затем, если мы удаляем любой из блоков \removefromtop, мы помещаем содержимое текущей страницы обратно в список последних добавлений, отменяем процедуру вывода и позволяем TeX продолжать работу как обычно, добавляя больше контента в нижнюю часть страницы.

решение2

Я думаю, проблема в вертикальном пробеле, добавленном после float. Поэтому в моем предложении ниже я изменяю это с помощью \myLengthи добавляю a \baselineskip.

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

решение3

После нескольких дней исследований, я думаю, что нашел решение своей проблемы. Идея такова:

В выходной процедуре \@cfltкоманда собирает, где это уместно, верхние плавающие элементы с основным текстом. Я изменил эту команду так, чтобы записать во вспомогательный файл координату Y, которая представляет начало основного текста (т.е. сразу после любых верхних плавающих элементов). Когда я запускаю \myCommand, я делаю то же самое и сохраняю координату Y, представляющую начало макроса, во вспомогательном файле.

При следующей компиляции я сравниваю две координаты Y. Если они равны, то \myCommandэто сразу после верхних поплавков, и я могу вставить, \vspace*{\dimexpr\myLength-\textfloatsep}как указано в вопросе.

Результат следующий:

введите описание изображения здесь

что более гармонично, чем исходный результат (хотя можно было бы внести незначительные коррективы):

введите описание изображения здесь


Однако во время моих тестов я обнаружил проблему, присущую приложению. Фактически, пространство, вставленное макросом, зависит от положения макроса на странице, которое, в свою очередь, зависит от пространства, вставленного макросом и т. д. Существует циклическая зависимость. Чтобы избежать этого, я полагаю, вам придется предотвратить разрыв страницы до или после \myCommandв зависимости от того, как вы хотите его использовать. Я открыт для предложений по улучшению.

Например, если в моем MWE для 4-го абзаца я использую \lipsum[2]вместо \lipsum[3], возникает нестабильность, и компиляция документов последовательно дает случаи A (слева) и B (справа) ниже:

введите описание изображения здесьвведите описание изображения здесь

Если мы начнем со случая A, макрос окажется в верхней части основного текста на странице 2.
Поэтому при следующей компиляции мы вставим пробел, равный , \myLength-\textfloatsepв начале \myCommand. Теперь вертикальный пробел \myLength-\textfloatsepменьше вертикального пробела \myLength, поэтому для команды достаточно места на странице 1, и мы находимся в случае B.
При следующей компиляции, поскольку мы больше не ищем верхний float, \myCommandвставляет вертикальный пробел, равный \myLength. На этот раз на странице 1 недостаточно места, и \myCommandон отправляется обратно в начало страницы 2, поэтому мы возвращаемся в случай A.


МВЭ:

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

Связанный контент